diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh index b697f22c009d1c..ab68a60dcfc279 100755 --- a/.ci/packer_cache.sh +++ b/.ci/packer_cache.sh @@ -44,6 +44,7 @@ tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \ x-pack/legacy/plugins/*/node_modules \ x-pack/legacy/plugins/reporting/.chromium \ test/plugin_functional/plugins/*/node_modules \ + examples/*/node_modules \ .es \ .chromedriver \ .geckodriver; diff --git a/.eslintrc.js b/.eslintrc.js index 106724c323d30e..88711c0959d68a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -131,12 +131,6 @@ module.exports = { 'react-hooks/exhaustive-deps': 'off', }, }, - { - files: ['src/plugins/eui_utils/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - }, - }, { files: ['src/plugins/kibana_react/**/*.{js,ts,tsx}'], rules: { @@ -170,13 +164,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', }, }, - { - files: ['x-pack/legacy/plugins/infra/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/exhaustive-deps': 'off', - 'react-hooks/rules-of-hooks': 'off', - }, - }, { files: ['x-pack/legacy/plugins/lens/**/*.{js,ts,tsx}'], rules: { @@ -209,13 +196,6 @@ module.exports = { 'react-hooks/rules-of-hooks': 'off', }, }, - { - files: ['x-pack/legacy/plugins/watcher/**/*.{js,ts,tsx}'], - rules: { - 'react-hooks/rules-of-hooks': 'off', - 'react-hooks/exhaustive-deps': 'off', - }, - }, /** * Prettier @@ -317,7 +297,8 @@ module.exports = { 'src/legacy/**/*', 'x-pack/**/*', '!x-pack/**/*.test.*', - 'src/plugins/**/(public|server)/**/*', + '!x-pack/test/**/*', + '(src|x-pack)/plugins/**/(public|server)/**/*', 'src/core/(public|server)/**/*', ], from: [ @@ -337,16 +318,35 @@ module.exports = { '!src/core/server/types', '!src/core/server/*.test.mocks.ts', - 'src/plugins/**/public/**/*', - '!src/plugins/**/public/index.{js,ts,tsx}', - - 'src/plugins/**/server/**/*', - '!src/plugins/**/server/index.{js,ts,tsx}', + '(src|x-pack)/plugins/**/(public|server)/**/*', + '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], allowSameFolder: true, + errorMessage: 'Plugins may only import from top-level public and server modules.', }, { - target: ['src/core/**/*'], + target: [ + '(src|x-pack)/plugins/**/*', + '!(src|x-pack)/plugins/*/server/**/*', + + 'src/legacy/core_plugins/**/*', + '!src/legacy/core_plugins/*/server/**/*', + '!src/legacy/core_plugins/*/index.{js,ts,tsx}', + + 'x-pack/legacy/plugins/**/*', + '!x-pack/legacy/plugins/*/server/**/*', + '!x-pack/legacy/plugins/*/index.{js,ts,tsx}', + ], + from: [ + 'src/core/server', + 'src/core/server/**/*', + '(src|x-pack)/plugins/*/server/**/*', + ], + errorMessage: + 'Server modules cannot be imported into client modules or shared modules.', + }, + { + target: ['src/**/*'], from: ['x-pack/**/*'], errorMessage: 'OSS cannot import x-pack files.', }, @@ -360,6 +360,11 @@ module.exports = { ], errorMessage: 'The core cannot depend on any plugins.', }, + { + target: ['(src|x-pack)/plugins/*/public/**/*'], + from: ['ui/**/*', 'uiExports/**/*'], + errorMessage: 'Plugins cannot import legacy UI code.', + }, { from: ['src/legacy/ui/**/*', 'ui/**/*'], target: [ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c5e6768c17d464..36a2cda841fa8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ # App /x-pack/legacy/plugins/lens/ @elastic/kibana-app /x-pack/legacy/plugins/graph/ @elastic/kibana-app +/src/plugins/share/ @elastic/kibana-app +/src/legacy/server/url_shortening/ @elastic/kibana-app /src/legacy/server/sample_data/ @elastic/kibana-app # App Architecture @@ -14,7 +16,6 @@ /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/share/ @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 @@ -28,7 +29,6 @@ /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/legacy/server/url_shortening/ @elastic/kibana-app-arch # APM /x-pack/legacy/plugins/apm/ @elastic/apm-ui @@ -97,15 +97,19 @@ /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security -# Kibana Stack Services -/src/dev/i18n @elastic/kibana-stack-services -/packages/kbn-analytics/ @elastic/kibana-stack-services -/src/legacy/core_plugins/ui_metric/ @elastic/kibana-stack-services -/src/plugins/usage_collection/ @elastic/kibana-stack-services -/x-pack/legacy/plugins/telemetry @elastic/kibana-stack-services -/x-pack/legacy/plugins/alerting @elastic/kibana-stack-services -/x-pack/legacy/plugins/actions @elastic/kibana-stack-services -/x-pack/legacy/plugins/task_manager @elastic/kibana-stack-services +# Kibana Localization +/src/dev/i18n @elastic/kibana-localization + +# Pulse +/packages/kbn-analytics/ @elastic/pulse +/src/legacy/core_plugins/ui_metric/ @elastic/pulse +/src/plugins/usage_collection/ @elastic/pulse +/x-pack/legacy/plugins/telemetry @elastic/pulse + +# Kibana Alerting Services +/x-pack/legacy/plugins/alerting @elastic/kibana-alerting-services +/x-pack/legacy/plugins/actions @elastic/kibana-alerting-services +/x-pack/legacy/plugins/task_manager @elastic/kibana-alerting-services # Design **/*.scss @elastic/kibana-design diff --git a/.github/labeler.yml b/.github/labeler.yml deleted file mode 100644 index 57b299912ae9e7..00000000000000 --- a/.github/labeler.yml +++ /dev/null @@ -1,13 +0,0 @@ -'Team:AppArch': -- src/plugins/data/**/* -- src/plugins/embeddable/**/* -- src/plugins/kibana_react/**/* -- src/plugins/navigation/**/* -- src/plugins/kibana_utils/**/* -- src/legacy/core_plugins/dashboard_embeddable_container/**/* -- src/legacy/core_plugins/data/**/* -- src/legacy/core_plugins/embeddable_api/**/* -- src/legacy/core_plugins/interpreter/**/* -- src/legacy/ui/public/index_patterns/**/* -- src/legacy/ui/public/indexed_array/**/* -- src/legacy/ui/public/new_platform/**/* diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml new file mode 100644 index 00000000000000..aea8a9cad6b1f2 --- /dev/null +++ b/.github/workflows/pr-project-assigner.yml @@ -0,0 +1,15 @@ +on: + pull_request: + types: [labeled, unlabeled] + +jobs: + assign_to_project: + runs-on: ubuntu-latest + name: Assign a PR to project based on label + steps: + - name: Assign to project + uses: elastic/github-actions/project-assigner@v1.0.0 + id: project_assigner + with: + issue-mappings: '[{"label": "Team:AppAch", "projectName": "kibana-app-arch", "columnId": 6173897}]' + ghToken: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml new file mode 100644 index 00000000000000..c7f17993249ebb --- /dev/null +++ b/.github/workflows/project-assigner.yml @@ -0,0 +1,17 @@ +on: + issues: + types: [labeled, unlabeled] + +jobs: + assign_to_project: + runs-on: ubuntu-latest + name: Assign issue or PR to project based on label + steps: + - name: Assign to project + uses: elastic/github-actions/project-assigner@v1.0.0 + id: project_assigner + with: + issue-mappings: '[{"label": "Team:AppArch", "projectName": "kibana-app-arch", "columnId": 6173895}]' + ghToken: ${{ secrets.GITHUB_TOKEN }} + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 599cf26970030e..06e08c85dafec0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -218,6 +218,7 @@ node scripts/makelogs --auth : > The default username and password combination are `elastic:changeme` > Make sure to execute `node scripts/makelogs` *after* elasticsearch is up and running! + ### Running Elasticsearch Remotely You can save some system resources, and the effort of generating sample data, if you have a remote Elasticsearch cluster to connect to. (**Elasticians: you do! Check with your team about where to find credentials**) @@ -239,6 +240,41 @@ kibana.index: '.{YourGitHubHandle}-kibana' xpack.task_manager.index: '.{YourGitHubHandle}-task-manager-kibana' ``` +### Running remote clusters +Setup remote clusters for cross cluster search (CCS) and cross cluster replication (CCR). + +Start your primary cluster by running: +```bash +yarn es snapshot -E path.data=../data_prod1 +``` + +Start your remote cluster by running: +```bash +yarn es snapshot -E transport.port=9500 -E http.port=9201 -E path.data=../data_prod2 +``` + +Once both clusters are running, start kibana. Kibana will connect to the primary cluster. + +Setup the remote cluster in Kibana from either `Management` -> `Elasticsearch` -> `Remote Clusters` UI or by running the following script in `Console`. +``` +PUT _cluster/settings +{ + "persistent": { + "cluster": { + "remote": { + "cluster_one": { + "seeds": [ + "localhost:9500" + ] + } + } + } + } +} +``` + +Follow the [cross-cluster search](https://www.elastic.co/guide/en/kibana/current/management-cross-cluster-search.html) instructions for setting up index patterns to search across clusters. + ### Running Kibana Start the development server. @@ -506,7 +542,7 @@ yarn test:browser --dev # remove the --dev flag to run them once and close * In System Preferences > Sharing, change your computer name to be something simple, e.g. "computer". * Run Kibana with `yarn start --host=computer.local` (substituting your computer name). * Now you can run your VM, open the browser, and navigate to `http://computer.local:5601` to test Kibana. -* Alternatively you can use browserstack +* Alternatively you can use browserstack #### Running Browser Automation Tests diff --git a/config/apm.js b/config/apm.js index 8efbbf87487e36..0cfcd759f163ba 100644 --- a/config/apm.js +++ b/config/apm.js @@ -42,19 +42,22 @@ const { join } = require('path'); const { execSync } = require('child_process'); const merge = require('lodash.merge'); -module.exports = merge({ - active: false, - serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', - // The secretToken below is intended to be hardcoded in this file even though - // it makes it public. This is not a security/privacy issue. Normally we'd - // instead disable the need for a secretToken in the APM Server config where - // the data is transmitted to, but due to how it's being hosted, it's easier, - // for now, to simply leave it in. - secretToken: 'R0Gjg46pE9K9wGestd', - globalLabels: {}, - centralConfig: false, - logUncaughtExceptions: true -}, devConfig()); +module.exports = merge( + { + active: false, + serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', + // The secretToken below is intended to be hardcoded in this file even though + // it makes it public. This is not a security/privacy issue. Normally we'd + // instead disable the need for a secretToken in the APM Server config where + // the data is transmitted to, but due to how it's being hosted, it's easier, + // for now, to simply leave it in. + secretToken: 'R0Gjg46pE9K9wGestd', + globalLabels: {}, + centralConfig: false, + logUncaughtExceptions: true, + }, + devConfig() +); const rev = gitRev(); if (rev !== null) module.exports.globalLabels.git_rev = rev; @@ -66,7 +69,10 @@ try { function gitRev() { try { - return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + return execSync('git rev-parse --short HEAD', { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); } catch (e) { return null; } diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 0e902e3608e729..ec0863b09d653c 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -42,3 +42,22 @@ Finally, this problem can also occur if you've changed the index name that you w The default index pattern can be found {apm-server-ref}/elasticsearch-output.html#index-option-es[here]. If you change this setting, you must also configure the `setup.template.name` and `setup.template.pattern` options. See {apm-server-ref}/configuration-template.html[Load the Elasticsearch index template]. + +==== Unknown route + +The {apm-app-ref}/transactions.html[transaction overview] will only display helpful information +when the transactions in your services are named correctly. +If you're seeing "GET unknown route" or "unknown route" in the APM app, +it could be a sign that something isn't working like it should. + +Elastic APM Agents come with built-in support for popular frameworks out-of-the-box. +This means, among other things, that the Agent will try to automatically name HTTP requests. +As an example, the Node.js Agent uses the route that handled the request, while the Java Agent uses the Servlet name. + +"Unknown route" indicates that the Agent can't determine what to name the request, +perhaps because the technology you're using isn't supported, the Agent has been installed incorrectly, +or because something is happening to the request that the Agent doesn't understand. + +To resolve this, you'll need to head over to the relevant {apm-agents-ref}[Agent documentation]. +Specifically, view the Agent's supported technologies page. +You can also use the Agent's public API to manually set a name for the transaction. diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md index c500c080a5feba..edab4f88497f64 100644 --- a/docs/development/core/public/kibana-plugin-public.app.md +++ b/docs/development/core/public/kibana-plugin-public.app.md @@ -17,5 +17,5 @@ export interface App extends AppBase | Property | Type | Description | | --- | --- | --- | | [chromeless](./kibana-plugin-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | -| [mount](./kibana-plugin-public.app.mount.md) | (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount> | A mount function called when the user navigates to this app's route. | +| [mount](./kibana-plugin-public.app.mount.md) | AppMount | AppMountDeprecated | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md). | diff --git a/docs/development/core/public/kibana-plugin-public.app.mount.md b/docs/development/core/public/kibana-plugin-public.app.mount.md index dda06b035db4a0..151fb7baeb138f 100644 --- a/docs/development/core/public/kibana-plugin-public.app.mount.md +++ b/docs/development/core/public/kibana-plugin-public.app.mount.md @@ -4,10 +4,15 @@ ## App.mount property -A mount function called when the user navigates to this app's route. +A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md). Signature: ```typescript -mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +mount: AppMount | AppMountDeprecated; ``` + +## Remarks + +When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). + diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.md index b53873bc0fb8ae..a63de399c2ecb4 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.md @@ -16,5 +16,5 @@ export interface ApplicationSetup | Method | Description | | --- | --- | | [register(app)](./kibana-plugin-public.applicationsetup.register.md) | Register an mountable application to the system. | -| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationsetup.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md index f264ba500ed6ef..275ba431bc7e7c 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationsetup.registermountcontext.md @@ -4,12 +4,16 @@ ## ApplicationSetup.registerMountContext() method -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. +> Warning: This API is now obsolete. +> +> + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +21,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.md b/docs/development/core/public/kibana-plugin-public.applicationstart.md index 2a60ff449e44ea..4baa4565ff7b06 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.md @@ -23,5 +23,5 @@ export interface ApplicationStart | --- | --- | | [getUrlForApp(appId, options)](./kibana-plugin-public.applicationstart.geturlforapp.md) | Returns a relative URL to a given app, including the global base path. | | [navigateToApp(appId, options)](./kibana-plugin-public.applicationstart.navigatetoapp.md) | Navigiate to a given app | -| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. | +| [registerMountContext(contextName, provider)](./kibana-plugin-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | diff --git a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md index 62821fcbb92bad..c15a23fe82b21d 100644 --- a/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.applicationstart.registermountcontext.md @@ -4,12 +4,16 @@ ## ApplicationStart.registerMountContext() method -Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. +> Warning: This API is now obsolete. +> +> + +Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: ```typescript -registerMountContext(contextName: T, provider: IContextProvider): void; +registerMountContext(contextName: T, provider: IContextProvider): void; ``` ## Parameters @@ -17,7 +21,7 @@ registerMountContext(contextName: T, provider: | Parameter | Type | Description | | --- | --- | --- | | contextName | T | The key of [AppMountContext](./kibana-plugin-public.appmountcontext.md) this provider's return value should be attached to. | -| provider | IContextProvider<App['mount'], T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | +| provider | IContextProvider<AppMountDeprecated, T> | A [IContextProvider](./kibana-plugin-public.icontextprovider.md) function | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.appmount.md b/docs/development/core/public/kibana-plugin-public.appmount.md new file mode 100644 index 00000000000000..25faa7be30b68f --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMount](./kibana-plugin-public.appmount.md) + +## AppMount type + +A mount function called when the user navigates to this app's route. + +Signature: + +```typescript +export declare type AppMount = (params: AppMountParameters) => AppUnmount | Promise; +``` diff --git a/docs/development/core/public/kibana-plugin-public.appmountcontext.md b/docs/development/core/public/kibana-plugin-public.appmountcontext.md index 68a1c27b118366..2f8c0553d0b382 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountcontext.md +++ b/docs/development/core/public/kibana-plugin-public.appmountcontext.md @@ -4,7 +4,11 @@ ## AppMountContext interface -The context object received when applications are mounted to the DOM. +> Warning: This API is now obsolete. +> +> + +The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). Signature: diff --git a/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md b/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md new file mode 100644 index 00000000000000..936642abcc97a3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.appmountdeprecated.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) + +## AppMountDeprecated type + +> Warning: This API is now obsolete. +> +> + +A mount function called when the user navigates to this app's route. + +Signature: + +```typescript +export declare type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; +``` + +## Remarks + +When function has two arguments, it will be called with a [context](./kibana-plugin-public.appmountcontext.md) as the first argument. This behavior is \*\*deprecated\*\*, and consumers should instead use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). + diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md index 31513bda2e8791..a1544373ee6980 100644 --- a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md +++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md @@ -22,9 +22,9 @@ export class MyPlugin implements Plugin { setup({ application }) { application.register({ id: 'my-app', - async mount(context, params) { + async mount(params) { const { renderApp } = await import('./application'); - return renderApp(context, params); + return renderApp(params); }, }); } @@ -38,7 +38,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Route } from 'react-router-dom'; -export renderApp = (context, { appBasePath, element }) => { +import { CoreStart, AppMountParams } from 'src/core/public'; +import { MyPluginDepsStart } from './plugin'; + +export renderApp = ({ appBasePath, element }: AppMountParams) => { ReactDOM.render( // pass `appBasePath` to `basename` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.context.md b/docs/development/core/public/kibana-plugin-public.coresetup.context.md index e56ecb92074c48..f2a891c6c674eb 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.context.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.context.md @@ -4,6 +4,10 @@ ## CoreSetup.context property +> Warning: This API is now obsolete. +> +> + [ContextSetup](./kibana-plugin-public.contextsetup.md) Signature: diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md b/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md new file mode 100644 index 00000000000000..b89d98b0a9ed53 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.getstartservices.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [getStartServices](./kibana-plugin-public.coresetup.getstartservices.md) + +## CoreSetup.getStartServices() method + +Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed `start`. + +Signature: + +```typescript +getStartServices(): Promise<[CoreStart, TPluginsStart]>; +``` +Returns: + +`Promise<[CoreStart, TPluginsStart]>` + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index 8314bde7b95f01..7d75782df2e321 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -9,7 +9,7 @@ Core services exposed to the `Plugin` setup lifecycle Signature: ```typescript -export interface CoreSetup +export interface CoreSetup ``` ## Properties @@ -24,3 +24,9 @@ export interface CoreSetup | [notifications](./kibana-plugin-public.coresetup.notifications.md) | NotificationsSetup | [NotificationsSetup](./kibana-plugin-public.notificationssetup.md) | | [uiSettings](./kibana-plugin-public.coresetup.uisettings.md) | IUiSettingsClient | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | +## Methods + +| Method | Description | +| --- | --- | +| [getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers, such as [App.mount](./kibana-plugin-public.app.mount.md). Promise will not resolve until Core and plugin dependencies have completed start. | + diff --git a/docs/development/core/public/kibana-plugin-public.httpbody.md b/docs/development/core/public/kibana-plugin-public.httpbody.md deleted file mode 100644 index ab31f28b8dc383..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpbody.md +++ /dev/null @@ -1,12 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpBody](./kibana-plugin-public.httpbody.md) - -## HttpBody type - - -Signature: - -```typescript -export declare type HttpBody = BodyInit | null | any; -``` diff --git a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md index aa669df796a09c..5b1ee898a444d6 100644 --- a/docs/development/core/public/kibana-plugin-public.httperrorresponse.md +++ b/docs/development/core/public/kibana-plugin-public.httperrorresponse.md @@ -8,7 +8,7 @@ Signature: ```typescript -export interface HttpErrorResponse extends HttpResponse +export interface HttpErrorResponse extends IHttpResponse ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md new file mode 100644 index 00000000000000..250cf83309b3c4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.asresponse.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) > [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) + +## HttpFetchOptions.asResponse property + +When `true` the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. + +Signature: + +```typescript +asResponse?: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md index eca29b37425e99..6a0c4a8a7f1379 100644 --- a/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md +++ b/docs/development/core/public/kibana-plugin-public.httpfetchoptions.md @@ -16,6 +16,7 @@ export interface HttpFetchOptions extends HttpRequestInit | Property | Type | Description | | --- | --- | --- | +| [asResponse](./kibana-plugin-public.httpfetchoptions.asresponse.md) | boolean | When true the return type of [HttpHandler](./kibana-plugin-public.httphandler.md) will be an [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) with detailed request and response information. When false, the return type will just be the parsed response body. Defaults to false. | | [headers](./kibana-plugin-public.httpfetchoptions.headers.md) | HttpHeadersInit | Headers to send with the request. See [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md). | | [prependBasePath](./kibana-plugin-public.httpfetchoptions.prependbasepath.md) | boolean | Whether or not the request should automatically prepend the basePath. Defaults to true. | | [query](./kibana-plugin-public.httpfetchoptions.query.md) | HttpFetchQuery | The query string for an HTTP request. See [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md). | diff --git a/docs/development/core/public/kibana-plugin-public.httphandler.md b/docs/development/core/public/kibana-plugin-public.httphandler.md index 80fd1ea2e5761e..89458c4743cd6e 100644 --- a/docs/development/core/public/kibana-plugin-public.httphandler.md +++ b/docs/development/core/public/kibana-plugin-public.httphandler.md @@ -2,12 +2,12 @@ [Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpHandler](./kibana-plugin-public.httphandler.md) -## HttpHandler type +## HttpHandler interface -A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response. +A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. Signature: ```typescript -export declare type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +export interface HttpHandler ``` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md index ca43ea31f0e2e8..3a67dcbad3119b 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.response.md @@ -9,17 +9,17 @@ Define an interceptor to be executed after a response is received. Signature: ```typescript -response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; +response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| httpResponse | HttpResponse | | +| httpResponse | IHttpResponse | | | controller | IHttpInterceptController | | Returns: -`Promise | InterceptedHttpResponse | void` +`Promise | IHttpResponseInterceptorOverrides | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md index b8abd50e454617..476ceba649d402 100644 --- a/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md +++ b/docs/development/core/public/kibana-plugin-public.httpinterceptor.responseerror.md @@ -9,7 +9,7 @@ Define an interceptor to be executed if a response interceptor throws an error o Signature: ```typescript -responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; +responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; ``` ## Parameters @@ -21,5 +21,5 @@ responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptC Returns: -`Promise | InterceptedHttpResponse | void` +`Promise | IHttpResponseInterceptorOverrides | void` diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.md b/docs/development/core/public/kibana-plugin-public.httpresponse.md deleted file mode 100644 index e44515cc8a1e02..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpresponse.md +++ /dev/null @@ -1,19 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) - -## HttpResponse interface - - -Signature: - -```typescript -export interface HttpResponse extends InterceptedHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [request](./kibana-plugin-public.httpresponse.request.md) | Readonly<Request> | | - diff --git a/docs/development/core/public/kibana-plugin-public.httpresponse.request.md b/docs/development/core/public/kibana-plugin-public.httpresponse.request.md deleted file mode 100644 index 84ab1bc7af8537..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.httpresponse.request.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [HttpResponse](./kibana-plugin-public.httpresponse.md) > [request](./kibana-plugin-public.httpresponse.request.md) - -## HttpResponse.request property - -Signature: - -```typescript -request: Readonly; -``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md new file mode 100644 index 00000000000000..2f8710ccdc60ef --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [body](./kibana-plugin-public.ihttpresponse.body.md) + +## IHttpResponse.body property + +Parsed body received, may be undefined if there was an error. + +Signature: + +```typescript +readonly body?: TResponseBody; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.md new file mode 100644 index 00000000000000..5ddce0ba2d0f14 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) + +## IHttpResponse interface + + +Signature: + +```typescript +export interface IHttpResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.ihttpresponse.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | +| [request](./kibana-plugin-public.ihttpresponse.request.md) | Readonly<Request> | Raw request sent to Kibana server. | +| [response](./kibana-plugin-public.ihttpresponse.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | + diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md new file mode 100644 index 00000000000000..12e5405eb5ed43 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.request.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [request](./kibana-plugin-public.ihttpresponse.request.md) + +## IHttpResponse.request property + +Raw request sent to Kibana server. + +Signature: + +```typescript +readonly request: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md b/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md new file mode 100644 index 00000000000000..9d0b4b59a638d7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponse.response.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) > [response](./kibana-plugin-public.ihttpresponse.response.md) + +## IHttpResponse.response property + +Raw response received, may be undefined if there was an error. + +Signature: + +```typescript +readonly response?: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md new file mode 100644 index 00000000000000..36fcfb390617c3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) > [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md) + +## IHttpResponseInterceptorOverrides.body property + +Parsed body received, may be undefined if there was an error. + +Signature: + +```typescript +readonly body?: TResponseBody; +``` diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md new file mode 100644 index 00000000000000..44f067c429e987 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) + +## IHttpResponseInterceptorOverrides interface + +Properties that can be returned by HttpInterceptor.request to override the response. + +Signature: + +```typescript +export interface IHttpResponseInterceptorOverrides +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-public.ihttpresponseinterceptoroverrides.body.md) | TResponseBody | Parsed body received, may be undefined if there was an error. | +| [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md) | Readonly<Response> | Raw response received, may be undefined if there was an error. | + diff --git a/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md new file mode 100644 index 00000000000000..bcba996645ba6d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) > [response](./kibana-plugin-public.ihttpresponseinterceptoroverrides.response.md) + +## IHttpResponseInterceptorOverrides.response property + +Raw response received, may be undefined if there was an error. + +Signature: + +```typescript +readonly response?: Readonly; +``` diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md deleted file mode 100644 index fc6d34c0b74f2a..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.body.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) > [body](./kibana-plugin-public.interceptedhttpresponse.body.md) - -## InterceptedHttpResponse.body property - -Signature: - -```typescript -body?: HttpBody; -``` diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md deleted file mode 100644 index c4a7f4d6b2afaf..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) - -## InterceptedHttpResponse interface - - -Signature: - -```typescript -export interface InterceptedHttpResponse -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [body](./kibana-plugin-public.interceptedhttpresponse.body.md) | HttpBody | | -| [response](./kibana-plugin-public.interceptedhttpresponse.response.md) | Response | | - diff --git a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md b/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md deleted file mode 100644 index dceb55113ee784..00000000000000 --- a/docs/development/core/public/kibana-plugin-public.interceptedhttpresponse.response.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) > [response](./kibana-plugin-public.interceptedhttpresponse.response.md) - -## InterceptedHttpResponse.response property - -Signature: - -```typescript -response?: Response; -``` diff --git a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md index f704bc65d12a52..a753300437c1c8 100644 --- a/docs/development/core/public/kibana-plugin-public.legacycoresetup.md +++ b/docs/development/core/public/kibana-plugin-public.legacycoresetup.md @@ -13,7 +13,7 @@ Setup interface exposed to the legacy platform via the `ui/new_platform` module. Signature: ```typescript -export interface LegacyCoreSetup extends CoreSetup +export interface LegacyCoreSetup extends CoreSetup ``` ## Properties diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index f527c92d070ded..2c43f36ede09e6 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -26,7 +26,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppBase](./kibana-plugin-public.appbase.md) | | | [ApplicationSetup](./kibana-plugin-public.applicationsetup.md) | | | [ApplicationStart](./kibana-plugin-public.applicationstart.md) | | -| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. | +| [AppMountContext](./kibana-plugin-public.appmountcontext.md) | The context object received when applications are mounted to the DOM. Deprecated, use [CoreSetup.getStartServices()](./kibana-plugin-public.coresetup.getstartservices.md). | | [AppMountParameters](./kibana-plugin-public.appmountparameters.md) | | | [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [ChromeBadge](./kibana-plugin-public.chromebadge.md) | | @@ -52,10 +52,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpErrorResponse](./kibana-plugin-public.httperrorresponse.md) | | | [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) | All options that may be used with a [HttpHandler](./kibana-plugin-public.httphandler.md). | | [HttpFetchQuery](./kibana-plugin-public.httpfetchquery.md) | | +| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) for the response. | | [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | | | [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md). | | [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)s. | -| [HttpResponse](./kibana-plugin-public.httpresponse.md) | | | [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | | [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @kbn/i18n and @elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. | | [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication | @@ -63,7 +63,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. | | [IHttpFetchError](./kibana-plugin-public.ihttpfetcherror.md) | | | [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md) | Used to halt a request Promise chain in a [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md). | -| [InterceptedHttpResponse](./kibana-plugin-public.interceptedhttpresponse.md) | | +| [IHttpResponse](./kibana-plugin-public.ihttpresponse.md) | | +| [IHttpResponseInterceptorOverrides](./kibana-plugin-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. | | [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-public.iuisettingsclient.md) | | [LegacyCoreSetup](./kibana-plugin-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the ui/new_platform module. | | [LegacyCoreStart](./kibana-plugin-public.legacycorestart.md) | Start interface exposed to the legacy platform via the ui/new_platform module. | @@ -88,6 +89,13 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsCreateOptions](./kibana-plugin-public.savedobjectscreateoptions.md) | | | [SavedObjectsFindOptions](./kibana-plugin-public.savedobjectsfindoptions.md) | | | [SavedObjectsFindResponsePublic](./kibana-plugin-public.savedobjectsfindresponsepublic.md) | Return type of the Saved Objects find() method.\*Note\*: this type is different between the Public and Server Saved Objects clients. | +| [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) | Represents a failure to import due to a conflict. | +| [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) | Represents a failure to import. | +| [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) | Represents a failure to import due to missing references. | +| [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) | The response describing the result of an import. | +| [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) | Describes a retry operation for importing a saved object. | +| [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) | Represents a failure to import due to an unknown reason. | +| [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) | Represents a failure to import due to having an unsupported saved object type. | | [SavedObjectsMigrationVersion](./kibana-plugin-public.savedobjectsmigrationversion.md) | Information about the migrations that have been applied to this SavedObject. When Kibana starts up, KibanaMigrator detects outdated documents and migrates them based on this value. For each migration that has been applied, the plugin's name is used as a key and the latest migration version as the value. | | [SavedObjectsStart](./kibana-plugin-public.savedobjectsstart.md) | | | [SavedObjectsUpdateOptions](./kibana-plugin-public.savedobjectsupdateoptions.md) | | @@ -97,6 +105,8 @@ The plugin integrates with the core system via lifecycle events: `setup` | Type Alias | Description | | --- | --- | +| [AppMount](./kibana-plugin-public.appmount.md) | A mount function called when the user navigates to this app's route. | +| [AppMountDeprecated](./kibana-plugin-public.appmountdeprecated.md) | A mount function called when the user navigates to this app's route. | | [AppUnmount](./kibana-plugin-public.appunmount.md) | A function called when an application should be unmounted from the page. This function should be synchronous. | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | | [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-public.chromehelpextensionmenucustomlink.md) | | @@ -108,8 +118,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. | | [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) | | [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md), excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md). | -| [HttpBody](./kibana-plugin-public.httpbody.md) | | -| [HttpHandler](./kibana-plugin-public.httphandler.md) | A function for making an HTTP requests to Kibana's backend. See [HttpFetchOptions](./kibana-plugin-public.httpfetchoptions.md) for options and [HttpBody](./kibana-plugin-public.httpbody.md) for the response. | | [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | | [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. | diff --git a/docs/development/core/public/kibana-plugin-public.plugin.setup.md b/docs/development/core/public/kibana-plugin-public.plugin.setup.md index 56855b02cfbad4..f058bc8d86fbce 100644 --- a/docs/development/core/public/kibana-plugin-public.plugin.setup.md +++ b/docs/development/core/public/kibana-plugin-public.plugin.setup.md @@ -7,14 +7,14 @@ Signature: ```typescript -setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; +setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| core | CoreSetup | | +| core | CoreSetup<TPluginsStart> | | | plugins | TPluginsSetup | | Returns: diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md index 1ce18834f53196..a4fa3f17d0d94f 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.find.md @@ -9,5 +9,5 @@ Search for objects Signature: ```typescript -find: (options: Pick) => Promise>; +find: (options: Pick) => Promise>; ``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md index 6033c667c1866c..3c4e33db4af91b 100644 --- a/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsclient.md @@ -20,7 +20,7 @@ export declare class SavedObjectsClient | [bulkGet](./kibana-plugin-public.savedobjectsclient.bulkget.md) | | (objects?: {
id: string;
type: string;
}[]) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>> | Returns an array of objects by id | | [create](./kibana-plugin-public.savedobjectsclient.create.md) | | <T extends SavedObjectAttributes>(type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise<SimpleSavedObject<T>> | Persists an object | | [delete](./kibana-plugin-public.savedobjectsclient.delete.md) | | (type: string, id: string) => Promise<{}> | Deletes an object | -| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "fields" | "searchFields" | "defaultSearchOperator" | "hasReference" | "sortField" | "perPage">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | +| [find](./kibana-plugin-public.savedobjectsclient.find.md) | | <T extends SavedObjectAttributes>(options: Pick<SavedObjectFindOptionsServer, "search" | "filter" | "type" | "page" | "perPage" | "sortField" | "fields" | "searchFields" | "hasReference" | "defaultSearchOperator">) => Promise<SavedObjectsFindResponsePublic<T>> | Search for objects | | [get](./kibana-plugin-public.savedobjectsclient.get.md) | | <T extends SavedObjectAttributes>(type: string, id: string) => Promise<SimpleSavedObject<T>> | Fetches a single object | ## Methods diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.md new file mode 100644 index 00000000000000..6becc3d5074617 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) + +## SavedObjectsImportConflictError interface + +Represents a failure to import due to a conflict. + +Signature: + +```typescript +export interface SavedObjectsImportConflictError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-public.savedobjectsimportconflicterror.type.md) | 'conflict' | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.type.md new file mode 100644 index 00000000000000..af20cc8fa8df27 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportconflicterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportConflictError](./kibana-plugin-public.savedobjectsimportconflicterror.md) > [type](./kibana-plugin-public.savedobjectsimportconflicterror.type.md) + +## SavedObjectsImportConflictError.type property + +Signature: + +```typescript +type: 'conflict'; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.error.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.error.md new file mode 100644 index 00000000000000..ece6016e8bf542 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.error.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) > [error](./kibana-plugin-public.savedobjectsimporterror.error.md) + +## SavedObjectsImportError.error property + +Signature: + +```typescript +error: SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.id.md new file mode 100644 index 00000000000000..995fe61745a006 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) > [id](./kibana-plugin-public.savedobjectsimporterror.id.md) + +## SavedObjectsImportError.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.md new file mode 100644 index 00000000000000..dee8bb1c79a57d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) + +## SavedObjectsImportError interface + +Represents a failure to import. + +Signature: + +```typescript +export interface SavedObjectsImportError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [error](./kibana-plugin-public.savedobjectsimporterror.error.md) | SavedObjectsImportConflictError | SavedObjectsImportUnsupportedTypeError | SavedObjectsImportMissingReferencesError | SavedObjectsImportUnknownError | | +| [id](./kibana-plugin-public.savedobjectsimporterror.id.md) | string | | +| [title](./kibana-plugin-public.savedobjectsimporterror.title.md) | string | | +| [type](./kibana-plugin-public.savedobjectsimporterror.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.title.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.title.md new file mode 100644 index 00000000000000..71fa13ad4a5d09 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) > [title](./kibana-plugin-public.savedobjectsimporterror.title.md) + +## SavedObjectsImportError.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.type.md new file mode 100644 index 00000000000000..fe98dc928e5f0b --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimporterror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportError](./kibana-plugin-public.savedobjectsimporterror.md) > [type](./kibana-plugin-public.savedobjectsimporterror.type.md) + +## SavedObjectsImportError.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.blocking.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.blocking.md new file mode 100644 index 00000000000000..76bd6e0939a96a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.blocking.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) > [blocking](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.blocking.md) + +## SavedObjectsImportMissingReferencesError.blocking property + +Signature: + +```typescript +blocking: Array<{ + type: string; + id: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.md new file mode 100644 index 00000000000000..58af9e9be0cc5e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) + +## SavedObjectsImportMissingReferencesError interface + +Represents a failure to import due to missing references. + +Signature: + +```typescript +export interface SavedObjectsImportMissingReferencesError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [blocking](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.blocking.md) | Array<{
type: string;
id: string;
}> | | +| [references](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.references.md) | Array<{
type: string;
id: string;
}> | | +| [type](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.type.md) | 'missing_references' | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.references.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.references.md new file mode 100644 index 00000000000000..f1dc3b454f7ed9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.references.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) > [references](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.references.md) + +## SavedObjectsImportMissingReferencesError.references property + +Signature: + +```typescript +references: Array<{ + type: string; + id: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.type.md new file mode 100644 index 00000000000000..340b36248d83e1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportmissingreferenceserror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportMissingReferencesError](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.md) > [type](./kibana-plugin-public.savedobjectsimportmissingreferenceserror.type.md) + +## SavedObjectsImportMissingReferencesError.type property + +Signature: + +```typescript +type: 'missing_references'; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.errors.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.errors.md new file mode 100644 index 00000000000000..c085fd0f8c3b46 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.errors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) > [errors](./kibana-plugin-public.savedobjectsimportresponse.errors.md) + +## SavedObjectsImportResponse.errors property + +Signature: + +```typescript +errors?: SavedObjectsImportError[]; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.md new file mode 100644 index 00000000000000..9733f11fd6b8f1 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) + +## SavedObjectsImportResponse interface + +The response describing the result of an import. + +Signature: + +```typescript +export interface SavedObjectsImportResponse +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [errors](./kibana-plugin-public.savedobjectsimportresponse.errors.md) | SavedObjectsImportError[] | | +| [success](./kibana-plugin-public.savedobjectsimportresponse.success.md) | boolean | | +| [successCount](./kibana-plugin-public.savedobjectsimportresponse.successcount.md) | number | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.success.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.success.md new file mode 100644 index 00000000000000..062b8ce3f7c72d --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.success.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) > [success](./kibana-plugin-public.savedobjectsimportresponse.success.md) + +## SavedObjectsImportResponse.success property + +Signature: + +```typescript +success: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.successcount.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.successcount.md new file mode 100644 index 00000000000000..c2c93859261758 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportresponse.successcount.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportResponse](./kibana-plugin-public.savedobjectsimportresponse.md) > [successCount](./kibana-plugin-public.savedobjectsimportresponse.successcount.md) + +## SavedObjectsImportResponse.successCount property + +Signature: + +```typescript +successCount: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.id.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.id.md new file mode 100644 index 00000000000000..2569152f17b156 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) > [id](./kibana-plugin-public.savedobjectsimportretry.id.md) + +## SavedObjectsImportRetry.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.md new file mode 100644 index 00000000000000..e2cad52f92f2da --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) + +## SavedObjectsImportRetry interface + +Describes a retry operation for importing a saved object. + +Signature: + +```typescript +export interface SavedObjectsImportRetry +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-public.savedobjectsimportretry.id.md) | string | | +| [overwrite](./kibana-plugin-public.savedobjectsimportretry.overwrite.md) | boolean | | +| [replaceReferences](./kibana-plugin-public.savedobjectsimportretry.replacereferences.md) | Array<{
type: string;
from: string;
to: string;
}> | | +| [type](./kibana-plugin-public.savedobjectsimportretry.type.md) | string | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.overwrite.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.overwrite.md new file mode 100644 index 00000000000000..9d1f96b2fcfcfc --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.overwrite.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) > [overwrite](./kibana-plugin-public.savedobjectsimportretry.overwrite.md) + +## SavedObjectsImportRetry.overwrite property + +Signature: + +```typescript +overwrite: boolean; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.replacereferences.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.replacereferences.md new file mode 100644 index 00000000000000..fe587ef8134cc5 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.replacereferences.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) > [replaceReferences](./kibana-plugin-public.savedobjectsimportretry.replacereferences.md) + +## SavedObjectsImportRetry.replaceReferences property + +Signature: + +```typescript +replaceReferences: Array<{ + type: string; + from: string; + to: string; + }>; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.type.md new file mode 100644 index 00000000000000..b84dac102483ae --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportretry.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportRetry](./kibana-plugin-public.savedobjectsimportretry.md) > [type](./kibana-plugin-public.savedobjectsimportretry.type.md) + +## SavedObjectsImportRetry.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.md new file mode 100644 index 00000000000000..e6837571717879 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) + +## SavedObjectsImportUnknownError interface + +Represents a failure to import due to an unknown reason. + +Signature: + +```typescript +export interface SavedObjectsImportUnknownError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [message](./kibana-plugin-public.savedobjectsimportunknownerror.message.md) | string | | +| [statusCode](./kibana-plugin-public.savedobjectsimportunknownerror.statuscode.md) | number | | +| [type](./kibana-plugin-public.savedobjectsimportunknownerror.type.md) | 'unknown' | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.message.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.message.md new file mode 100644 index 00000000000000..976c2817bda0a2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) > [message](./kibana-plugin-public.savedobjectsimportunknownerror.message.md) + +## SavedObjectsImportUnknownError.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.statuscode.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.statuscode.md new file mode 100644 index 00000000000000..6c7255dd4b6313 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.statuscode.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) > [statusCode](./kibana-plugin-public.savedobjectsimportunknownerror.statuscode.md) + +## SavedObjectsImportUnknownError.statusCode property + +Signature: + +```typescript +statusCode: number; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.type.md new file mode 100644 index 00000000000000..2ef764d68322e6 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunknownerror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnknownError](./kibana-plugin-public.savedobjectsimportunknownerror.md) > [type](./kibana-plugin-public.savedobjectsimportunknownerror.type.md) + +## SavedObjectsImportUnknownError.type property + +Signature: + +```typescript +type: 'unknown'; +``` diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md new file mode 100644 index 00000000000000..09ae53c0313528 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) + +## SavedObjectsImportUnsupportedTypeError interface + +Represents a failure to import due to having an unsupported saved object type. + +Signature: + +```typescript +export interface SavedObjectsImportUnsupportedTypeError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [type](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.type.md) | 'unsupported_type' | | + diff --git a/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.type.md b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.type.md new file mode 100644 index 00000000000000..55ddf15058faba --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.savedobjectsimportunsupportedtypeerror.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [SavedObjectsImportUnsupportedTypeError](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.md) > [type](./kibana-plugin-public.savedobjectsimportunsupportedtypeerror.type.md) + +## SavedObjectsImportUnsupportedTypeError.type property + +Signature: + +```typescript +type: 'unsupported_type'; +``` diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecation.md b/docs/development/core/server/kibana-plugin-server.configdeprecation.md new file mode 100644 index 00000000000000..ba7e40b8dc6247 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecation.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) + +## ConfigDeprecation type + +Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration. + +Signature: + +```typescript +export declare type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; +``` + +## Remarks + +This should only be manually implemented if [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) does not provide the proper helpers for a specific deprecation need. + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md new file mode 100644 index 00000000000000..f022d6c1d064a3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) + +## ConfigDeprecationFactory interface + +Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md). + +See methods documentation for more detailed examples. + +Signature: + +```typescript +export interface ConfigDeprecationFactory +``` + +## Methods + +| 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. | +| [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. | + +## Example + + +```typescript +const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + rename('oldKey', 'newKey'), + unused('deprecatedKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md new file mode 100644 index 00000000000000..5bbbad763c545b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.rename.md @@ -0,0 +1,36 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [rename](./kibana-plugin-server.configdeprecationfactory.rename.md) + +## ConfigDeprecationFactory.rename() method + +Rename a configuration property from inside a plugin's configuration path. Will log a deprecation warning if the oldKey was found and deprecation applied. + +Signature: + +```typescript +rename(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'myplugin.oldKey' to 'myplugin.newKey' + +```typescript +const provider: ConfigDeprecationProvider = ({ rename }) => [ + rename('oldKey', 'newKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md new file mode 100644 index 00000000000000..d35ba25256fa1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.renamefromroot.md @@ -0,0 +1,38 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [renameFromRoot](./kibana-plugin-server.configdeprecationfactory.renamefromroot.md) + +## ConfigDeprecationFactory.renameFromRoot() method + +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. + +Signature: + +```typescript +renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| oldKey | string | | +| newKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Rename 'oldplugin.key' to 'newplugin.key' + +```typescript +const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [ + renameFromRoot('oldplugin.key', 'newplugin.key'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md new file mode 100644 index 00000000000000..0381480e84c4dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unused.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [unused](./kibana-plugin-server.configdeprecationfactory.unused.md) + +## ConfigDeprecationFactory.unused() method + +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. + +Signature: + +```typescript +unused(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'myplugin.deprecatedKey' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unused }) => [ + unused('deprecatedKey'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md new file mode 100644 index 00000000000000..ed37638b07375b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationfactory.unusedfromroot.md @@ -0,0 +1,37 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) > [unusedFromRoot](./kibana-plugin-server.configdeprecationfactory.unusedfromroot.md) + +## ConfigDeprecationFactory.unusedFromRoot() method + +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. + +Signature: + +```typescript +unusedFromRoot(unusedKey: string): ConfigDeprecation; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| unusedKey | string | | + +Returns: + +`ConfigDeprecation` + +## Example + +Flags 'somepath.deprecatedProperty' as unused + +```typescript +const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ + unusedFromRoot('somepath.deprecatedProperty'), +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md new file mode 100644 index 00000000000000..d2bb2a4e441b38 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationlogger.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) + +## ConfigDeprecationLogger type + +Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) + +Signature: + +```typescript +export declare type ConfigDeprecationLogger = (message: string) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md new file mode 100644 index 00000000000000..f5da9e9452bb51 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.configdeprecationprovider.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) + +## ConfigDeprecationProvider type + +A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md). + +See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. + +Signature: + +```typescript +export declare type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; +``` + +## Example + + +```typescript +const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + rename('oldKey', 'newKey'), + unused('deprecatedKey'), + myCustomDeprecation, +] + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index fceabd1237665d..06dcede0f2dfe7 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -46,6 +46,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Capabilities](./kibana-plugin-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-server.capabilities.md) that will be used by the application.Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the registerProvider method.Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the registerSwitcher method.Refers to the methods documentation for complete description and examples. | | [CapabilitiesStart](./kibana-plugin-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-server.capabilities.md). | +| [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) | Provides helpers to generates the most commonly used [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) when invoking a [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md).See methods documentation for more detailed examples. | | [ContextSetup](./kibana-plugin-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CoreSetup](./kibana-plugin-server.coresetup.md) | Context passed to the plugins setup method. | | [CoreStart](./kibana-plugin-server.corestart.md) | Context passed to the plugins start method. | @@ -82,7 +83,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | -| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration schema and capabilities. | +| [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | | [PluginInitializerContext](./kibana-plugin-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | | @@ -152,6 +153,9 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResult](./kibana-plugin-server.authresult.md) | | | [CapabilitiesProvider](./kibana-plugin-server.capabilitiesprovider.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | | [CapabilitiesSwitcher](./kibana-plugin-server.capabilitiesswitcher.md) | See [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | +| [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | Configuration deprecation returned from [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) that handles a single deprecation from the configuration. | +| [ConfigDeprecationLogger](./kibana-plugin-server.configdeprecationlogger.md) | Logger interface used when invoking a [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) | +| [ConfigDeprecationProvider](./kibana-plugin-server.configdeprecationprovider.md) | A provider that should returns a list of [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md).See [ConfigDeprecationFactory](./kibana-plugin-server.configdeprecationfactory.md) for more usage examples. | | [ConfigPath](./kibana-plugin-server.configpath.md) | | | [ElasticsearchClientConfig](./kibana-plugin-server.elasticsearchclientconfig.md) | | | [GetAuthHeaders](./kibana-plugin-server.getauthheaders.md) | Get headers to authenticate a user against Elasticsearch. | @@ -197,5 +201,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsClientContract](./kibana-plugin-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientWrapperFactory](./kibana-plugin-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | +| [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) | | | [UiSettingsType](./kibana-plugin-server.uisettingstype.md) | UI element type to represent the settings. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md new file mode 100644 index 00000000000000..00574101838f29 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.deprecations.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) > [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) + +## PluginConfigDescriptor.deprecations property + +Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. + +Signature: + +```typescript +deprecations?: ConfigDeprecationProvider; +``` diff --git a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md index 41fdcfe5df45d4..671298a67381ac 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md +++ b/docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md @@ -4,7 +4,7 @@ ## PluginConfigDescriptor interface -Describes a plugin configuration schema and capabilities. +Describes a plugin configuration properties. Signature: @@ -16,6 +16,7 @@ export interface PluginConfigDescriptor | Property | Type | Description | | --- | --- | --- | +| [deprecations](./kibana-plugin-server.pluginconfigdescriptor.deprecations.md) | ConfigDeprecationProvider | Provider for the [ConfigDeprecation](./kibana-plugin-server.configdeprecation.md) to apply to the plugin configuration. | | [exposeToBrowser](./kibana-plugin-server.pluginconfigdescriptor.exposetobrowser.md) | {
[P in keyof T]?: boolean;
} | List of configuration properties that will be available on the client-side plugin. | | [schema](./kibana-plugin-server.pluginconfigdescriptor.schema.md) | PluginConfigSchema<T> | Schema to use to validate the plugin configuration.[PluginConfigSchema](./kibana-plugin-server.pluginconfigschema.md) | @@ -39,6 +40,10 @@ export const config: PluginConfigDescriptor = { uiProp: true, }, schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('securityKey', 'secret'), + unused('deprecatedProperty'), + ], }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md b/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md new file mode 100644 index 00000000000000..418d406d4c8905 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.sharedglobalconfig.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SharedGlobalConfig](./kibana-plugin-server.sharedglobalconfig.md) + +## SharedGlobalConfig type + + +Signature: + +```typescript +export declare type SharedGlobalConfig = RecursiveReadonly<{ + kibana: Pick; + elasticsearch: Pick; + path: Pick; +}>; +``` diff --git a/docs/images/kibana-status-page-7_5_0.png b/docs/images/kibana-status-page-7_5_0.png new file mode 100644 index 00000000000000..2dac4c3f94c351 Binary files /dev/null and b/docs/images/kibana-status-page-7_5_0.png differ diff --git a/docs/images/kibana-status-page.png b/docs/images/kibana-status-page.png deleted file mode 100644 index b269dbd3573039..00000000000000 Binary files a/docs/images/kibana-status-page.png and /dev/null differ diff --git a/docs/maps/images/top_hits.png b/docs/maps/images/top_hits.png new file mode 100644 index 00000000000000..45bbf575f10dd5 Binary files /dev/null and b/docs/maps/images/top_hits.png differ diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index 5284bd9ac2ac55..22b736032cb796 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -101,11 +101,6 @@ aggregation. The remaining default settings are good, but there are a couple of settings that you might want to change. -. Under *Source settings* > *Grid resolution*, select from the different heat map resolutions. -+ -The default "Coarse" looks -good, but feel free to select a different resolution. - . Play around with the *Layer Style* > *Color range* setting. + diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index cd01acb2df7de5..98aa21f6a07a35 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -34,18 +34,23 @@ The point location is the weighted centroid for all geo-points in the gridded ce [role="xpack"] [[maps-top-hits-aggregation]] -=== Most recent entities +=== Top hits per entity -Most recent entities uses {es} {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation] to group your documents by entity. -Then, {ref}/search-aggregations-metrics-top-hits-aggregation.html[top hits metric aggregation] accumulates the most recent documents for each entry. +You can display the most relevant documents per entity, for example, the most recent GPS tracks per flight. +To get this data, {es} first groups your data using a {ref}/search-aggregations-bucket-terms-aggregation.html[terms aggregation], +then accumulates the most relevant documents based on sort order for each entry using a {ref}/search-aggregations-metrics-top-hits-aggregation.html[top hits metric aggregation]. -Most recent entities is available for <> with *Documents* source. -To enable most recent entities, click "Show most recent documents by entity" and configure the following: +Top hits per entity is available for <> with *Documents* source. +To enable top hits: +. In *Sorting*, select the *Show documents per entity* checkbox. . Set *Entity* to the field that identifies entities in your documents. This field will be used in the terms aggregation to group your documents into entity buckets. . Set *Documents per entity* to configure the maximum number of documents accumulated per entity. +[role="screenshot"] +image::maps/images/top_hits.png[] + [role="xpack"] [[point-to-point]] === Point to point diff --git a/docs/maps/search.asciidoc b/docs/maps/search.asciidoc index 97b10e389963eb..8a93352798d2c8 100644 --- a/docs/maps/search.asciidoc +++ b/docs/maps/search.asciidoc @@ -89,7 +89,7 @@ The most common cause for empty layers are searches for a field that exists in o You can prevent the search bar from applying search context to a layer by configuring the following: -* In *Source settings*, clear the *Apply global filter to source* checkbox to turn off the global search context for the layer source. +* In *Filtering*, clear the *Apply global filter to layer data* checkbox to turn off the global search context for the layer source. * In *Term joins*, clear the *Apply global filter to join* checkbox to turn off the global search context for the <>. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index a2c05e4d873250..d6dd4378da1b7c 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -50,15 +50,17 @@ this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). `xpack.security.session.idleTimeout`:: -Sets the session duration (in milliseconds). By default, sessions stay active -until the browser is closed. When this is set to an explicit idle timeout, closing -the browser still requires the user to log back in to {kib}. +Sets the session duration. The format is a string of `[ms|s|m|h|d|w|M|Y]` +(e.g. '70ms', '5s', '3d', '1Y'). By default, sessions stay active until the +browser is closed. When this is set to an explicit idle timeout, closing the +browser still requires the user to log back in to {kib}. `xpack.security.session.lifespan`:: -Sets the maximum duration (in milliseconds), also known as "absolute timeout". By -default, a session can be renewed indefinitely. When this value is set, a session -will end once its lifespan is exceeded, even if the user is not idle. NOTE: if -`idleTimeout` is not set, this setting will still cause sessions to expire. +Sets the maximum duration, also known as "absolute timeout". The format is a +string of `[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). By default, +a session can be renewed indefinitely. When this value is set, a session will end +once its lifespan is exceeded, even if the user is not idle. NOTE: if `idleTimeout` +is not set, this setting will still cause sessions to expire. `xpack.security.loginAssistanceMessage`:: Adds a message to the login screen. Useful for displaying information about maintenance windows, links to corporate sign up pages etc. diff --git a/docs/setup/access.asciidoc b/docs/setup/access.asciidoc index 538b42781b127a..a7374a37ddaec4 100644 --- a/docs/setup/access.asciidoc +++ b/docs/setup/access.asciidoc @@ -2,8 +2,8 @@ == Accessing Kibana Kibana is a web application that you access through port 5601. All you need to do is point your web browser at the -machine where Kibana is running and specify the port number. For example, `localhost:5601` or -`http://YOURDOMAIN.com:5601`. +machine where Kibana is running and specify the port number. For example, `localhost:5601` or `http://YOURDOMAIN.com:5601`. +If you want to allow remote users to connect, set the parameter `server.host` in `kibana.yml` to a non-loopback address. When you access Kibana, the <> page loads by default with the default index pattern selected. The time filter is set to the last 15 minutes and the search query is set to match-all (\*). @@ -15,9 +15,10 @@ If you still don't see any results, it's possible that you don't *have* any docu [[status]] === Checking Kibana Status -You can reach the Kibana server's status page by navigating to `localhost:5601/status`. The status page displays +You can reach the Kibana server's status page by navigating to the status endpoint, for example, `localhost:5601/status`. The status page displays information about the server's resource usage and lists the installed plugins. -image::images/kibana-status-page.png[] +[role="screenshot"] +image::images/kibana-status-page-7_5_0.png[] NOTE: For JSON-formatted server status details, use the API endpoint at `localhost:5601/api/status` diff --git a/docs/setup/production.asciidoc b/docs/setup/production.asciidoc index 67afe0896a0ddf..fed4ba4886bf94 100644 --- a/docs/setup/production.asciidoc +++ b/docs/setup/production.asciidoc @@ -6,6 +6,7 @@ * <> * <> * <> +* <> How you deploy Kibana largely depends on your use case. If you are the only user, you can run Kibana on your local machine and configure it to point to whatever @@ -27,7 +28,7 @@ You can use {stack} {security-features} to control what {es} data users can access through Kibana. When {security-features} are enabled, Kibana users have to log in. They need to -have a role granting <> as well as access +have a role granting <> as well as access to the indices they will be working with in Kibana. If a user loads a Kibana dashboard that accesses data in an index that they @@ -125,4 +126,17 @@ elasticsearch.hosts: -------- Related configurations include `elasticsearch.sniffInterval`, `elasticsearch.sniffOnStart`, and `elasticsearch.sniffOnConnectionFault`. -These can be used to automatically update the list of hosts as a cluster is resized. Parameters can be found on the {kibana-ref}/settings.html[settings page]. \ No newline at end of file +These can be used to automatically update the list of hosts as a cluster is resized. Parameters can be found on the {kibana-ref}/settings.html[settings page]. + +[float] +[[memory]] +=== Memory +Kibana has a default maximum memory limit of 1.4 GB, and in most cases, we recommend leaving this unconfigured. In some scenarios, such as large reporting jobs, +it may make sense to tweak limits to meet more specific requirements. + +You can modify this limit by setting `--max-old-space-size` in the `NODE_OPTIONS` environment variable. For deb and rpm, packages this is passed in via `/etc/default/kibana` and can be appended to the bottom of the file. + +The option accepts a limit in MB: +-------- +NODE_OPTIONS="--max-old-space-size=2048" bin/kibana +-------- diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 5cda7b2b214f0b..414d4ef34db555 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -7,9 +7,7 @@ if you installed {kib} from an archive distribution (`.tar.gz` or `.zip`), by default it is in `$KIBANA_HOME/config`. By default, with package distributions (Debian or RPM), it is in `/etc/kibana`. -The default settings configure Kibana to run on `localhost:5601`. To change the -host or port number, or connect to Elasticsearch running on a different machine, -you'll need to update your `kibana.yml` file. You can also enable SSL and set a +The default host and port settings configure {kib} to run on `localhost:5601`. To change this behavior and allow remote users to connect, you'll need to update your `kibana.yml` file. You can also enable SSL and set a variety of other options. Finally, environment variables can be injected into configuration using `${MY_ENV_VAR}` syntax. @@ -32,7 +30,7 @@ strongly recommend that you keep the default CSP rules that ship with Kibana. `csp.strict:`:: *Default: `true`* Blocks access to Kibana to any browser that does not enforce even rudimentary CSP rules. In practice, this will disable -support for older, less safe browsers like Internet Explorer. +support for older, less safe browsers like Internet Explorer. See <> for more information. `csp.warnLegacyBrowsers:`:: *Default: `true`* Shows a warning message after @@ -65,7 +63,7 @@ connects to this Kibana instance. `elasticsearch.requestHeadersWhitelist:`:: *Default: `[ 'authorization' ]`* List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side headers, set this value to [] (an empty list). -Removing the `authorization` header from being whitelisted means that you cannot +Removing the `authorization` header from being whitelisted means that you cannot use <> in Kibana. `elasticsearch.requestTimeout:`:: *Default: 30000* Time in milliseconds to wait @@ -164,19 +162,19 @@ The following example shows a valid logging rotate configuration: enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` that feature would not take any effect. -`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the +`logging.rotate.everyBytes:`:: [experimental] *Default: 10485760* The maximum size of a log file (that is `not an exact` limit). After the limit is reached, a new log file is generated. The default size limit is 10485760 (10 MB) and this option should be at least greater than 1024. -`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep -on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` +`logging.rotate.keepFiles:`:: [experimental] *Default: 7* The number of most recent rotated log files to keep +on disk. Older files are deleted during log rotation. The default value is 7. The `logging.rotate.keepFiles` option has to be in the range of 2 to 1024 files. -`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case +`logging.rotate.pollingInterval:`:: [experimental] *Default: 10000* The number of milliseconds for the polling strategy in case the `logging.rotate.usePolling` is enabled. That option has to be in the range of 5000 to 3600000 milliseconds. -`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring -the log file. However, there is some systems where it could not be always accurate. In those cases, if needed, +`logging.rotate.usePolling:`:: [experimental] *Default: false* By default we try to understand the best way to monitoring +the log file. However, there is some systems where it could not be always accurate. In those cases, if needed, the `polling` method could be used enabling that option. `logging.silent:`:: *Default: false* Set the value of this setting to `true` to @@ -304,7 +302,7 @@ This setting may not be used when `server.compression.enabled` is set to `false` send on all responses to the client from the Kibana server. `server.host:`:: *Default: "localhost"* This setting specifies the host of the -back end server. +back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. `server.keepaliveTimeout:`:: *Default: "120000"* The number of milliseconds to wait for additional data before restarting the `server.socketTimeout` counter. @@ -358,15 +356,15 @@ supported protocols with versions. Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2 setting this to `true` enables unauthenticated users to access the Kibana server status API and status page. -`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, -users are able to change the telemetry setting at a later time in -<>. If `false`, -{kib} looks at the value of `telemetry.optIn` to determine whether to send +`telemetry.allowChangingOptInStatus`:: *Default: true*. If `true`, +users are able to change the telemetry setting at a later time in +<>. If `false`, +{kib} looks at the value of `telemetry.optIn` to determine whether to send telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` cannot be `false` at the same time. -`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. - If `false`, collection of telemetry data is disabled. +`telemetry.optIn`:: *Default: true* If `true`, telemetry data is sent to Elastic. + If `false`, collection of telemetry data is disabled. To enable telemetry and prevent users from disabling it, set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index 32f341a9c1b7cc..2e2aaf688e8b68 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -163,7 +163,7 @@ required by {kib}. If you want to use Third Party initiated SSO , then you must + [source,yaml] -------------------------------------------------------------------------------- -server.xsrf.whitelist: [/api/security/v1/oidc] +server.xsrf.whitelist: [/api/security/oidc/initiate_login] -------------------------------------------------------------------------------- [float] diff --git a/docs/user/security/images/reporting-privileges-example.png b/docs/user/security/images/reporting-privileges-example.png new file mode 100644 index 00000000000000..d108fe6634fa2b Binary files /dev/null and b/docs/user/security/images/reporting-privileges-example.png differ diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index aaba60ca4b3cac..86599be9af3754 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -9,19 +9,60 @@ To use {reporting} with {security} enabled, you need to <>. If you are automatically generating reports with {ref}/xpack-alerting.html[{watcher}], you also need to configure {watcher} -to trust the {kib} server's certificate. For more information, see +to trust the {kib} server's certificate. +//// +For more information, see <>. +//// [[reporting-app-users]] -To enable users to generate reports, assign them the built-in `reporting_user` -role. Users will also need the appropriate <> to access the objects +To enable users to generate reports, you must assign them the built-in `reporting_user` +role. Users will also need the appropriate <> to access the objects to report on and the {es} indices. -* If you're using the `native` realm, you can assign roles through -**Management > Users** UI in Kibana or with the `user` API. For example, -the following request creates a `reporter` user that has the -`reporting_user` role and the `kibana_user` role: +[float] +[[reporting-roles-management-ui]] +=== If you are using the `native` realm + +You can assign roles through the +*Management* app in Kibana or with the <>. +This example shows how to use *Management* to create a user who has a custom role and the +`reporting_user` role. + +. Go to *Management > Roles*, and click *Create role*. + +. Give the new role a name, for example, `custom_reporting_user`. + +. Specify the indices and privileges. + +Access to data is an index-level privilege, so in *Create role*, +add a line for each index that contains the data for the report and give each +index `read` and `view_index_metadata` privileges. +For more information, see {ref}/security-privileges.html[Security privileges]. ++ +[role="screenshot"] +image::user/security/images/reporting-privileges-example.png["Reporting privileges"] + +. Add space privileges. ++ +Reporting users typically save searches, create +visualizations, and build dashboards. They require a space +that provides read and write privileges in +*Discover*, *Visualize*, and *Dashboard*. + +. Save your new role. + +. Create a user account with the proper roles. ++ +Go to *Management > Users*, add a new user, and assign the user the built-in +`reporting_user` role and your new custom role, `custom_reporting_user`. + +[float] +[[reporting-roles-user-api]] +==== With the user API +This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the +`reporting_user` role and the `kibana_user` role: + [source, sh] --------------------------------------------------------------- POST /_security/user/reporter @@ -32,13 +73,17 @@ POST /_security/user/reporter } --------------------------------------------------------------- -* If you are using an LDAP or Active Directory realm, you can either assign +[float] +=== If you are using an external identity provider + +If you are using an external identity provider, such as +LDAP or Active Directory, you can either assign roles on a per user basis, or assign roles to groups of users. By default, role mappings are configured in {ref}/mapping-roles.html[`config/shield/role_mapping.yml`]. For example, the following snippet assigns the user named Bill Murray the `kibana_user` and `reporting_user` roles: -+ + [source,yaml] -------------------------------------------------------------------------------- kibana_user: diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index 60f5473f43b9df..a68a2ee285ee38 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -59,13 +59,14 @@ For more information, see <>. . Optional: Set a timeout to expire idle sessions. By default, a session stays active until the browser is closed. To define a sliding session expiration, set the `xpack.security.session.idleTimeout` property in the `kibana.yml` -configuration file. The idle timeout is specified in milliseconds. For example, -set the idle timeout to 600000 to expire idle sessions after 10 minutes: +configuration file. The idle timeout is formatted as a duration of +`[ms|s|m|h|d|w|M|Y]` (e.g. '70ms', '5s', '3d', '1Y'). For example, set +the idle timeout to expire idle sessions after 10 minutes: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.session.idleTimeout: 600000 +xpack.security.session.idleTimeout: "10m" -------------------------------------------------------------------------------- -- @@ -74,13 +75,14 @@ the "absolute timeout". By default, a session stays active until the browser is closed. If an idle timeout is defined, a session can still be extended indefinitely. To define a maximum session lifespan, set the `xpack.security.session.lifespan` property in the `kibana.yml` configuration -file. The lifespan is specified in milliseconds. For example, set the lifespan -to 28800000 to expire sessions after 8 hours: +file. The lifespan is formatted as a duration of `[ms|s|m|h|d|w|M|Y]` +(e.g. '70ms', '5s', '3d', '1Y'). For example, set the lifespan to expire +sessions after 8 hours: + -- [source,yaml] -------------------------------------------------------------------------------- -xpack.security.session.lifespan: 28800000 +xpack.security.session.lifespan: "8h" -------------------------------------------------------------------------------- -- diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000000000..7cade0b35f8209 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +## Example plugins + +This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag, via + +``` +yarn start --run-examples +``` + diff --git a/examples/demo_search/README.md b/examples/demo_search/README.md new file mode 100644 index 00000000000000..f0b461e3287b40 --- /dev/null +++ b/examples/demo_search/README.md @@ -0,0 +1,8 @@ +## Demo search strategy + +This example registers a custom search strategy that simply takes a name string in the request and returns the +string `Hello {name}` + +To see the demo search strategy in action, navigate to the `Search explorer` app. + +To run these examples, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/test/plugin_functional/plugins/demo_search/common/index.ts b/examples/demo_search/common/index.ts similarity index 90% rename from test/plugin_functional/plugins/demo_search/common/index.ts rename to examples/demo_search/common/index.ts index 9254412ece291a..6587ee96ef61ba 100644 --- a/test/plugin_functional/plugins/demo_search/common/index.ts +++ b/examples/demo_search/common/index.ts @@ -17,10 +17,7 @@ * under the License. */ -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../src/plugins/data/public'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../../../src/plugins/data/public'; export const DEMO_SEARCH_STRATEGY = 'DEMO_SEARCH_STRATEGY'; diff --git a/test/plugin_functional/plugins/demo_search/kibana.json b/examples/demo_search/kibana.json similarity index 100% rename from test/plugin_functional/plugins/demo_search/kibana.json rename to examples/demo_search/kibana.json diff --git a/test/plugin_functional/plugins/demo_search/package.json b/examples/demo_search/package.json similarity index 88% rename from test/plugin_functional/plugins/demo_search/package.json rename to examples/demo_search/package.json index 1f4fa1421906ad..404002a50e7103 100644 --- a/test/plugin_functional/plugins/demo_search/package.json +++ b/examples/demo_search/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "scripts": { - "kbn": "node ../../../../scripts/kbn.js", + "kbn": "node ../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { diff --git a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts b/examples/demo_search/public/demo_search_strategy.ts similarity index 96% rename from test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts rename to examples/demo_search/public/demo_search_strategy.ts index 298eaaaf420e04..d2854151e14c85 100644 --- a/test/plugin_functional/plugins/demo_search/public/demo_search_strategy.ts +++ b/examples/demo_search/public/demo_search_strategy.ts @@ -22,8 +22,8 @@ import { ISearchContext, SYNC_SEARCH_STRATEGY, ISearchGeneric, -} from '../../../../../src/plugins/data/public'; -import { TSearchStrategyProvider, ISearchStrategy } from '../../../../../src/plugins/data/public'; +} from '../../../src/plugins/data/public'; +import { TSearchStrategyProvider, ISearchStrategy } from '../../../src/plugins/data/public'; import { DEMO_SEARCH_STRATEGY, IDemoResponse } from '../common'; diff --git a/test/plugin_functional/plugins/demo_search/public/index.ts b/examples/demo_search/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/demo_search/public/index.ts rename to examples/demo_search/public/index.ts diff --git a/test/plugin_functional/plugins/demo_search/public/plugin.ts b/examples/demo_search/public/plugin.ts similarity index 92% rename from test/plugin_functional/plugins/demo_search/public/plugin.ts rename to examples/demo_search/public/plugin.ts index 37f8d3955708ab..81ba585b991902 100644 --- a/test/plugin_functional/plugins/demo_search/public/plugin.ts +++ b/examples/demo_search/public/plugin.ts @@ -17,8 +17,8 @@ * under the License. */ -import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; -import { Plugin, CoreSetup, PluginInitializerContext } from '../../../../../src/core/public'; +import { DataPublicPluginSetup } from '../../../src/plugins/data/public'; +import { Plugin, CoreSetup, PluginInitializerContext } from '../../../src/core/public'; import { DEMO_SEARCH_STRATEGY } from '../common'; import { demoClientSearchStrategyProvider } from './demo_search_strategy'; import { IDemoRequest, IDemoResponse } from '../common'; @@ -36,7 +36,7 @@ interface DemoDataSearchSetupDependencies { * If the caller does not pass in the right `request` shape, typescript will * complain. The caller will also get a typed response. */ -declare module '../../../../../src/plugins/data/public' { +declare module '../../../src/plugins/data/public' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; } diff --git a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts b/examples/demo_search/server/demo_search_strategy.ts similarity index 94% rename from test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts rename to examples/demo_search/server/demo_search_strategy.ts index d3f2360add6c03..5b0883be1fc514 100644 --- a/test/plugin_functional/plugins/demo_search/server/demo_search_strategy.ts +++ b/examples/demo_search/server/demo_search_strategy.ts @@ -17,7 +17,7 @@ * under the License. */ -import { TSearchStrategyProvider } from 'src/plugins/data/server'; +import { TSearchStrategyProvider } from '../../../src/plugins/data/server'; import { DEMO_SEARCH_STRATEGY } from '../common'; export const demoSearchStrategyProvider: TSearchStrategyProvider = () => { diff --git a/test/plugin_functional/plugins/demo_search/server/index.ts b/examples/demo_search/server/index.ts similarity index 100% rename from test/plugin_functional/plugins/demo_search/server/index.ts rename to examples/demo_search/server/index.ts diff --git a/test/plugin_functional/plugins/demo_search/server/plugin.ts b/examples/demo_search/server/plugin.ts similarity index 97% rename from test/plugin_functional/plugins/demo_search/server/plugin.ts rename to examples/demo_search/server/plugin.ts index c6628e7c768201..23c82225563c89 100644 --- a/test/plugin_functional/plugins/demo_search/server/plugin.ts +++ b/examples/demo_search/server/plugin.ts @@ -35,7 +35,7 @@ interface IDemoSearchExplorerDeps { * If the caller does not pass in the right `request` shape, typescript will * complain. The caller will also get a typed response. */ -declare module '../../../../../src/plugins/data/server' { +declare module '../../../src/plugins/data/server' { export interface IRequestTypesMap { [DEMO_SEARCH_STRATEGY]: IDemoRequest; } diff --git a/test/plugin_functional/plugins/demo_search/tsconfig.json b/examples/demo_search/tsconfig.json similarity index 75% rename from test/plugin_functional/plugins/demo_search/tsconfig.json rename to examples/demo_search/tsconfig.json index 304ffdc0a299d4..7fa03739119b43 100644 --- a/test/plugin_functional/plugins/demo_search/tsconfig.json +++ b/examples/demo_search/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../../../tsconfig.json", + "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./target", "skipLibCheck": true @@ -10,7 +10,7 @@ "public/**/*.ts", "public/**/*.tsx", "server/**/*.ts", - "../../../../typings/**/*" + "../../typings/**/*" ], "exclude": [] } diff --git a/examples/search_explorer/README.md b/examples/search_explorer/README.md new file mode 100644 index 00000000000000..0e5a48cf22dc1a --- /dev/null +++ b/examples/search_explorer/README.md @@ -0,0 +1,8 @@ +## Search explorer + +This example search explorer app shows how to use different search strategies in order to retrieve data. + +One demo uses the built in elasticsearch search strategy, and runs a search against data in elasticsearch. The +other demo uses the custom demo search strategy, a custom search strategy registerd inside the [demo_search plugin](../demo_search). + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/test/plugin_functional/plugins/search_explorer/kibana.json b/examples/search_explorer/kibana.json similarity index 100% rename from test/plugin_functional/plugins/search_explorer/kibana.json rename to examples/search_explorer/kibana.json diff --git a/test/plugin_functional/plugins/search_explorer/package.json b/examples/search_explorer/package.json similarity index 87% rename from test/plugin_functional/plugins/search_explorer/package.json rename to examples/search_explorer/package.json index 9a5e0e83a2207f..62d0127c30cc6d 100644 --- a/test/plugin_functional/plugins/search_explorer/package.json +++ b/examples/search_explorer/package.json @@ -8,7 +8,7 @@ }, "license": "Apache-2.0", "scripts": { - "kbn": "node ../../../../scripts/kbn.js", + "kbn": "node ../../scripts/kbn.js", "build": "rm -rf './target' && tsc" }, "devDependencies": { diff --git a/test/plugin_functional/plugins/search_explorer/public/application.tsx b/examples/search_explorer/public/application.tsx similarity index 97% rename from test/plugin_functional/plugins/search_explorer/public/application.tsx rename to examples/search_explorer/public/application.tsx index 4762209a548c1b..801a3c615ac613 100644 --- a/test/plugin_functional/plugins/search_explorer/public/application.tsx +++ b/examples/search_explorer/public/application.tsx @@ -28,7 +28,7 @@ import { EuiSideNav, } from '@elastic/eui'; -import { AppMountContext, AppMountParameters } from '../../../../../src/core/public'; +import { AppMountContext, AppMountParameters } from '../../../src/core/public'; import { EsSearchTest } from './es_strategy'; import { Page } from './page'; import { DemoStrategy } from './demo_strategy'; diff --git a/test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx b/examples/search_explorer/public/demo_strategy.tsx similarity index 98% rename from test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx rename to examples/search_explorer/public/demo_strategy.tsx index 8a0dd31e3595f2..7c6c21d2b7aedb 100644 --- a/test/plugin_functional/plugins/search_explorer/public/demo_strategy.tsx +++ b/examples/search_explorer/public/demo_strategy.tsx @@ -25,7 +25,7 @@ import { EuiFlexGroup, EuiFieldText, } from '@elastic/eui'; -import { ISearchGeneric } from '../../../../../src/plugins/data/public'; +import { ISearchGeneric } from '../../../src/plugins/data/public'; import { DoSearch } from './do_search'; import { GuideSection } from './guide_section'; diff --git a/test/plugin_functional/plugins/search_explorer/public/do_search.tsx b/examples/search_explorer/public/do_search.tsx similarity index 97% rename from test/plugin_functional/plugins/search_explorer/public/do_search.tsx rename to examples/search_explorer/public/do_search.tsx index e039e4ff3f63f1..f279b9fcd6e239 100644 --- a/test/plugin_functional/plugins/search_explorer/public/do_search.tsx +++ b/examples/search_explorer/public/do_search.tsx @@ -21,10 +21,7 @@ import React from 'react'; import { EuiButton, EuiCodeBlock, EuiFlexItem, EuiFlexGroup, EuiText } from '@elastic/eui'; import { EuiProgress } from '@elastic/eui'; import { Observable } from 'rxjs'; -import { - IKibanaSearchResponse, - IKibanaSearchRequest, -} from '../../../../../src/plugins/data/public'; +import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../../src/plugins/data/public'; interface Props { request: IKibanaSearchRequest; diff --git a/test/plugin_functional/plugins/search_explorer/public/documentation.tsx b/examples/search_explorer/public/documentation.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/documentation.tsx rename to examples/search_explorer/public/documentation.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx b/examples/search_explorer/public/es_strategy.tsx similarity index 87% rename from test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx rename to examples/search_explorer/public/es_strategy.tsx index d35c67191a1f80..e26c11a646669d 100644 --- a/test/plugin_functional/plugins/search_explorer/public/es_strategy.tsx +++ b/examples/search_explorer/public/es_strategy.tsx @@ -29,19 +29,19 @@ import { ISearchGeneric, IEsSearchResponse, IEsSearchRequest, -} from '../../../../../src/plugins/data/public'; +} from '../../../src/plugins/data/public'; import { DoSearch } from './do_search'; import { GuideSection } from './guide_section'; // @ts-ignore -import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_service'; +import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_service'; // @ts-ignore -import serverStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/es_search/es_search_strategy'; +import serverStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/es_search/es_search_strategy'; // @ts-ignore -import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_service'; +import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_service'; // @ts-ignore -import publicStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/es_search/es_search_strategy'; +import publicStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/es_search/es_search_strategy'; interface Props { search: ISearchGeneric; diff --git a/test/plugin_functional/plugins/search_explorer/public/guide_section.tsx b/examples/search_explorer/public/guide_section.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/guide_section.tsx rename to examples/search_explorer/public/guide_section.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/index.ts b/examples/search_explorer/public/index.ts similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/index.ts rename to examples/search_explorer/public/index.ts diff --git a/test/plugin_functional/plugins/search_explorer/public/page.tsx b/examples/search_explorer/public/page.tsx similarity index 100% rename from test/plugin_functional/plugins/search_explorer/public/page.tsx rename to examples/search_explorer/public/page.tsx diff --git a/test/plugin_functional/plugins/search_explorer/public/plugin.tsx b/examples/search_explorer/public/plugin.tsx similarity index 94% rename from test/plugin_functional/plugins/search_explorer/public/plugin.tsx rename to examples/search_explorer/public/plugin.tsx index cbe1073aa186b0..a7a6fd11341a4b 100644 --- a/test/plugin_functional/plugins/search_explorer/public/plugin.tsx +++ b/examples/search_explorer/public/plugin.tsx @@ -18,7 +18,7 @@ */ import { Plugin, CoreSetup } from 'kibana/public'; -import { ISearchAppMountContext } from '../../../../../src/plugins/data/public'; +import { ISearchAppMountContext } from '../../../src/plugins/data/public'; declare module 'kibana/public' { interface AppMountContext { diff --git a/test/plugin_functional/plugins/search_explorer/public/search_api.tsx b/examples/search_explorer/public/search_api.tsx similarity index 70% rename from test/plugin_functional/plugins/search_explorer/public/search_api.tsx rename to examples/search_explorer/public/search_api.tsx index 8ec6225d1f172d..fc68571e4ef68a 100644 --- a/test/plugin_functional/plugins/search_explorer/public/search_api.tsx +++ b/examples/search_explorer/public/search_api.tsx @@ -20,22 +20,22 @@ import React from 'react'; import { GuideSection } from './guide_section'; // @ts-ignore -import publicSetupContract from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_setup'; +import publicSetupContract from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_setup'; // @ts-ignore -import publicSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search_strategy'; +import publicSearchStrategy from '!!raw-loader!./../../../src/plugins/data/public/search/i_search_strategy'; // @ts-ignore -import publicSearch from '!!raw-loader!./../../../../../src/plugins/data/public/search/i_search'; +import publicSearch from '!!raw-loader!./../../../src/plugins/data/public/search/i_search'; // @ts-ignore -import publicPlugin from '!!raw-loader!./../../../../../src/plugins/data/public/search/search_service'; +import publicPlugin from '!!raw-loader!./../../../src/plugins/data/public/search/search_service'; // @ts-ignore -import serverSetupContract from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_setup'; +import serverSetupContract from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_setup'; // @ts-ignore -import serverSearchStrategy from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search_strategy'; +import serverSearchStrategy from '!!raw-loader!./../../../src/plugins/data/server/search/i_search_strategy'; // @ts-ignore -import serverSearch from '!!raw-loader!./../../../../../src/plugins/data/server/search/i_search'; +import serverSearch from '!!raw-loader!./../../../src/plugins/data/server/search/i_search'; // @ts-ignore -import serverPlugin from '!!raw-loader!./../../../../../src/plugins/data/server/search/search_service'; +import serverPlugin from '!!raw-loader!./../../../src/plugins/data/server/search/search_service'; export const SearchApiPage = () => ( - str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; - -run( - async ({ log, flags }) => { - await withProcRunner(log, async proc => { - log.info('Deleting old output'); - await del(BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); - await Promise.all([ - ...['web', 'node'].map(subTask => - proc.run(padRight(10, `babel:${subTask}`), { - cmd: 'babel', - args: [ - 'src', - '--config-file', - require.resolve('../babel.config.js'), - '--out-dir', - resolve(BUILD_DIR, subTask), - '--extensions', - '.ts,.js,.tsx', - ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), - ], - wait: true, - env: { - ...env, - BABEL_ENV: subTask, - }, - cwd, - }) - ), - - proc.run(padRight(10, 'tsc'), { - cmd: 'tsc', - args: [ - '--emitDeclarationOnly', - ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), - ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), - ], - wait: true, - env, - cwd, - }), - ]); - - log.success('Complete'); - }); - }, - { - description: 'Simple build tool for @kbn/analytics package', - flags: { - boolean: ['watch', 'source-maps'], - help: ` - --watch Run in watch mode - --source-maps Include sourcemaps - `, - }, - } -); +/* + * 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 { resolve } = require('path'); + +const del = require('del'); +const supportsColor = require('supports-color'); +const { run, withProcRunner } = require('@kbn/dev-utils'); + +const ROOT_DIR = resolve(__dirname, '..'); +const BUILD_DIR = resolve(ROOT_DIR, 'target'); + +const padRight = (width, str) => + str.length >= width ? str : `${str}${' '.repeat(width - str.length)}`; + +run( + async ({ log, flags }) => { + await withProcRunner(log, async proc => { + log.info('Deleting old output'); + await del(BUILD_DIR); + + const cwd = ROOT_DIR; + const env = { ...process.env }; + if (supportsColor.stdout) { + env.FORCE_COLOR = 'true'; + } + + log.info(`Starting babel and typescript${flags.watch ? ' in watch mode' : ''}`); + await Promise.all([ + ...['web', 'node'].map(subTask => + proc.run(padRight(10, `babel:${subTask}`), { + cmd: 'babel', + args: [ + 'src', + '--config-file', + require.resolve('../babel.config.js'), + '--out-dir', + resolve(BUILD_DIR, subTask), + '--extensions', + '.ts,.js,.tsx', + ...(flags.watch ? ['--watch'] : ['--quiet']), + ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), + ], + wait: true, + env: { + ...env, + BABEL_ENV: subTask, + }, + cwd, + }) + ), + + proc.run(padRight(10, 'tsc'), { + cmd: 'tsc', + args: [ + '--emitDeclarationOnly', + ...(flags.watch ? ['--watch', '--preserveWatchOutput', 'true'] : []), + ...(flags['source-maps'] ? ['--declarationMap', 'true'] : []), + ], + wait: true, + env, + cwd, + }), + ]); + + log.success('Complete'); + }); + }, + { + description: 'Simple build tool for @kbn/analytics package', + flags: { + boolean: ['watch', 'source-maps'], + help: ` + --watch Run in watch mode + --source-maps Include sourcemaps + `, + }, + } +); diff --git a/packages/kbn-analytics/src/index.ts b/packages/kbn-analytics/src/index.ts index 6514347b0b1272..c7a1350841168e 100644 --- a/packages/kbn-analytics/src/index.ts +++ b/packages/kbn-analytics/src/index.ts @@ -20,3 +20,4 @@ export { ReportHTTP, Reporter, ReporterConfig } from './reporter'; export { UiStatsMetricType, METRIC_TYPE } from './metrics'; export { Report, ReportManager } from './report'; +export { Storage } from './storage'; diff --git a/packages/kbn-analytics/src/report.ts b/packages/kbn-analytics/src/report.ts index 333bc05d28f9be..1c0b37966355fb 100644 --- a/packages/kbn-analytics/src/report.ts +++ b/packages/kbn-analytics/src/report.ts @@ -23,23 +23,25 @@ const REPORT_VERSION = 1; export interface Report { reportVersion: typeof REPORT_VERSION; - uiStatsMetrics: { - [key: string]: { + uiStatsMetrics?: Record< + string, + { key: string; appName: string; eventName: string; type: UiStatsMetricType; stats: Stats; - }; - }; - userAgent?: { - [key: string]: { + } + >; + userAgent?: Record< + string, + { userAgent: string; key: string; type: METRIC_TYPE.USER_AGENT; appName: string; - }; - }; + } + >; } export class ReportManager { @@ -49,14 +51,15 @@ export class ReportManager { this.report = report || ReportManager.createReport(); } static createReport(): Report { - return { reportVersion: REPORT_VERSION, uiStatsMetrics: {} }; + return { reportVersion: REPORT_VERSION }; } public clearReport() { this.report = ReportManager.createReport(); } public isReportEmpty(): boolean { - const noUiStats = Object.keys(this.report.uiStatsMetrics).length === 0; - const noUserAgent = !this.report.userAgent || Object.keys(this.report.userAgent).length === 0; + const { uiStatsMetrics, userAgent } = this.report; + const noUiStats = !uiStatsMetrics || Object.keys(uiStatsMetrics).length === 0; + const noUserAgent = !userAgent || Object.keys(userAgent).length === 0; return noUiStats && noUserAgent; } private incrementStats(count: number, stats?: Stats): Stats { @@ -113,14 +116,17 @@ export class ReportManager { case METRIC_TYPE.LOADED: case METRIC_TYPE.COUNT: { const { appName, type, eventName, count } = metric; - const existingStats = (report.uiStatsMetrics[key] || {}).stats; - this.report.uiStatsMetrics[key] = { - key, - appName, - eventName, - type, - stats: this.incrementStats(count, existingStats), - }; + if (report.uiStatsMetrics) { + const existingStats = (report.uiStatsMetrics[key] || {}).stats; + this.report.uiStatsMetrics = this.report.uiStatsMetrics || {}; + this.report.uiStatsMetrics[key] = { + key, + appName, + eventName, + type, + stats: this.incrementStats(count, existingStats), + }; + } return; } default: diff --git a/packages/kbn-analytics/src/storage.ts b/packages/kbn-analytics/src/storage.ts index 9abf9fa7dac2ce..5c18d9280ffc75 100644 --- a/packages/kbn-analytics/src/storage.ts +++ b/packages/kbn-analytics/src/storage.ts @@ -19,7 +19,13 @@ import { Report } from './report'; -export type Storage = Map; +export interface Storage { + get: (key: string) => T | null; + set: (key: string, value: T) => S; + remove: (key: string) => T | null; + clear: () => void; +} + export class ReportStorageManager { storageKey: string; private storage?: Storage; diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index e554859928c0b7..c8bb4a568c5003 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -14,6 +14,7 @@ "@babel/preset-typescript": "^7.3.3", "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", + "babel-plugin-styled-components": "^1.10.6", "babel-plugin-transform-define": "^1.3.1", "babel-plugin-typescript-strip-namespaces": "^1.1.1" } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index def848f4154bbb..e6a8bd81b602ea 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -33,6 +33,12 @@ module.exports = () => { plugins: [ require.resolve('@babel/plugin-transform-modules-commonjs'), require.resolve('@babel/plugin-syntax-dynamic-import'), - ] + [ + require.resolve('babel-plugin-styled-components'), + { + fileName: false, + }, + ], + ], }; }; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index 25962c91a896d7..4244006f4a3a33 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -62,8 +62,7 @@ export interface ReqOptions { query?: Record; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; - attempt?: number; - maxAttempts?: number; + retries?: number; } const delay = (ms: number) => @@ -87,44 +86,47 @@ export class KbnClientRequester { async request(options: ReqOptions): Promise { const url = Url.resolve(this.pickUrl(), options.path); const description = options.description || `${options.method} ${url}`; - const attempt = options.attempt === undefined ? 1 : options.attempt; - const maxAttempts = - options.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : options.maxAttempts; - - try { - const response = await Axios.request({ - method: options.method, - url, - data: options.body, - params: options.query, - headers: { - 'kbn-xsrf': 'kbn-client', - }, - }); - - return response.data; - } catch (error) { - let retryErrorMsg: string | undefined; - if (isAxiosRequestError(error)) { - retryErrorMsg = `[${description}] request failed (attempt=${attempt})`; - } else if (isConcliftOnGetError(error)) { - retryErrorMsg = `Conflict on GET (path=${options.path}, attempt=${attempt})`; - } + let attempt = 0; + const maxAttempts = options.retries ?? DEFAULT_MAX_ATTEMPTS; + + while (true) { + attempt += 1; + + try { + const response = await Axios.request({ + method: options.method, + url, + data: options.body, + params: options.query, + headers: { + 'kbn-xsrf': 'kbn-client', + }, + }); + + return response.data; + } catch (error) { + const conflictOnGet = isConcliftOnGetError(error); + const requestedRetries = options.retries !== undefined; + const failedToGetResponse = isAxiosRequestError(error); + + let errorMessage; + if (conflictOnGet) { + errorMessage = `Conflict on GET (path=${options.path}, attempt=${attempt}/${maxAttempts})`; + this.log.error(errorMessage); + } else if (requestedRetries || failedToGetResponse) { + errorMessage = `[${description}] request failed (attempt=${attempt}/${maxAttempts})`; + this.log.error(errorMessage); + } else { + throw error; + } - if (retryErrorMsg) { if (attempt < maxAttempts) { - this.log.error(retryErrorMsg); await delay(1000 * attempt); - return await this.request({ - ...options, - attempt: attempt + 1, - }); + continue; } - throw new Error(retryErrorMsg + ' and ran out of retries'); + throw new Error(`${errorMessage} -- and ran out of retries`); } - - throw error; } } } diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts index 03033bc5c2ccc4..ad01dea624c3c6 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_ui_settings.ts @@ -40,7 +40,7 @@ export class KbnClientUiSettings { async get(setting: string) { const all = await this.getAll(); - const value = all.settings[setting] ? all.settings[setting].userValue : undefined; + const value = all[setting]?.userValue; this.log.verbose('uiSettings.value: %j', value); return value; @@ -68,24 +68,24 @@ export class KbnClientUiSettings { * with some defaults */ async replace(doc: UiSettingValues) { - const all = await this.getAll(); - for (const [name, { isOverridden }] of Object.entries(all.settings)) { - if (!isOverridden) { - await this.unset(name); + this.log.debug('replacing kibana config doc: %j', doc); + + const changes: Record = { + ...this.defaults, + ...doc, + }; + + for (const [name, { isOverridden }] of Object.entries(await this.getAll())) { + if (!isOverridden && !changes.hasOwnProperty(name)) { + changes[name] = null; } } - this.log.debug('replacing kibana config doc: %j', doc); - await this.requester.request({ method: 'POST', path: '/api/kibana/settings', - body: { - changes: { - ...this.defaults, - ...doc, - }, - }, + body: { changes }, + retries: 5, }); } @@ -105,9 +105,11 @@ export class KbnClientUiSettings { } private async getAll() { - return await this.requester.request({ + const resp = await this.requester.request({ path: '/api/kibana/settings', method: 'GET', }); + + return resp.settings; } } diff --git a/packages/kbn-dev-utils/tsconfig.json b/packages/kbn-dev-utils/tsconfig.json index 40a3bd475f1c1d..4c519a609d86fe 100644 --- a/packages/kbn-dev-utils/tsconfig.json +++ b/packages/kbn-dev-utils/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "target", + "target": "ES2019", "declaration": true }, "include": [ diff --git a/packages/kbn-i18n/scripts/build.js b/packages/kbn-i18n/scripts/build.js index f4260d31d80fbf..ccdddc87dbc18a 100644 --- a/packages/kbn-i18n/scripts/build.js +++ b/packages/kbn-i18n/scripts/build.js @@ -55,7 +55,7 @@ run( '--extensions', '.ts,.js,.tsx', ...(flags.watch ? ['--watch'] : ['--quiet']), - ...(flags['source-maps'] ? ['--source-map', 'inline'] : []), + ...(flags['source-maps'] ? ['--source-maps', 'inline'] : []), ], wait: true, env: { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index bbe12a93c241f7..aea85c13d7f325 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -23185,6 +23185,7 @@ function getProjectPaths(rootPath, options = {}) { projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/plugin_functional/plugins/*')); projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'test/interpreter_functional/plugins/*')); + projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'examples/*')); if (!ossOnly) { projectPaths.push(Object(path__WEBPACK_IMPORTED_MODULE_0__["resolve"])(rootPath, 'x-pack')); diff --git a/packages/kbn-pm/src/config.ts b/packages/kbn-pm/src/config.ts index 4886b0c266a892..2e42a182e7ec37 100644 --- a/packages/kbn-pm/src/config.ts +++ b/packages/kbn-pm/src/config.ts @@ -44,6 +44,7 @@ export function getProjectPaths(rootPath: string, options: IProjectPathOptions = // correct and the expect behavior. projectPaths.push(resolve(rootPath, 'test/plugin_functional/plugins/*')); projectPaths.push(resolve(rootPath, 'test/interpreter_functional/plugins/*')); + projectPaths.push(resolve(rootPath, 'examples/*')); if (!ossOnly) { projectPaths.push(resolve(rootPath, 'x-pack')); diff --git a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml index 9da63234e03d46..3bfc686f9e845b 100644 --- a/packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml +++ b/packages/kbn-test/src/failed_tests_reporter/__fixtures__/ftr_report.xml @@ -18,7 +18,7 @@ Wait timed out after 10055ms at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)]]> - + { =================================================================== --- ftr.xml [object Object] +++ ftr.xml - @@ -2,52 +2,56 @@ + @@ -1,53 +1,56 @@ + ‹?xml version="1.0" encoding="utf-8"?› ‹testsuites› ‹testsuite timestamp="2019-06-05T23:37:10" time="903.670" tests="129" failures="5" skipped="71"› ‹testcase name="maps app maps loaded from sample data ecommerce "before all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps/sample_data·js" time="154.378"› - ‹system-out› + - ‹system-out› - ‹![CDATA[[00:00:00] │ + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + + + [00:00:00] │ [00:07:04] └-: maps app ... @@ -94,15 +99,10 @@ it('rewrites ftr reports with minimal changes', async () => { at process._tickCallback (internal/process/next_tick.js:68:7) at lastError (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:28:9) - at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13)]]› - - ‹/failure› + at onFailure (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-ciGroup7/node/immutable/kibana/test/common/services/retry/retry_for_success.ts:68:13) - + - + - +Failed Tests Reporter: - + - foo bar - +‹/failure› + ‹/failure› ‹/testcase› - ‹testcase name="maps app "after all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps" time="0.179"› + ‹testcase name="maps app "after all" hook" classname="Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps" time="0.179" metadata-json="{"messages":["foo"],"screenshots":[{"name":"failure[dashboard app using current data dashboard snapshots compare TSVB snapshot]","url":"https://storage.googleapis.com/kibana-ci-artifacts/jobs/elastic+kibana+7.x/1632/kibana-oss-tests/test/functional/screenshots/failure/dashboard%20app%20using%20current%20data%20dashboard%20snapshots%20compare%20TSVB%20snapshot.png"}]}"› ‹system-out› - ‹![CDATA[[00:00:00] │ + [00:00:00] │ @@ -181,11 +181,11 @@ it('rewrites jest reports with minimal changes', async () => { + ‹failure›‹![CDATA[ + TypeError: Cannot read property '0' of undefined + at Object.‹anonymous›.test (/var/lib/jenkins/workspace/elastic+kibana+master/JOB/x-pack-intake/node/immutable/kibana/x-pack/legacy/plugins/code/server/lsp/abstract_launcher.test.ts:166:10) - + - + - +Failed Tests Reporter: + + ]]›‹/failure› + + ‹system-out›Failed Tests Reporter: + - foo bar - +]]›‹/failure› + + + +‹/system-out› ‹/testcase› ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher can start and end a process" time="0.435"/› ‹testcase classname="X-Pack Jest Tests.x-pack/legacy/plugins/code/server/lsp" name="passive launcher should restart a process if a process died before connected" time="1.502"/› @@ -216,12 +216,17 @@ it('rewrites mocha reports with minimal changes', async () => { =================================================================== --- mocha.xml [object Object] +++ mocha.xml - @@ -2,12 +2,12 @@ + @@ -1,13 +1,16 @@ + ‹?xml version="1.0" encoding="utf-8"?› ‹testsuites› ‹testsuite timestamp="2019-06-13T23:29:36" time="30.739" tests="1444" failures="2" skipped="3"› ‹testcase name="code in multiple nodes "before all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.121"› - ‹system-out› + - ‹system-out› - ‹![CDATA[]]› + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + + + ‹/system-out› - ‹failure› @@ -232,7 +237,7 @@ it('rewrites mocha reports with minimal changes', async () => { ‹head›‹title›503 Service Temporarily Unavailable‹/title›‹/head› ‹body bgcolor="white"› ‹center›‹h1›503 Service Temporarily Unavailable‹/h1›‹/center› - @@ -15,24 +15,28 @@ + @@ -15,24 +18,24 @@ ‹/body› ‹/html› @@ -240,11 +245,7 @@ it('rewrites mocha reports with minimal changes', async () => { - at process._tickCallback (internal/process/next_tick.js:68:7)]]› - ‹/failure› + at process._tickCallback (internal/process/next_tick.js:68:7) - + - + - +Failed Tests Reporter: - + - foo bar - +]]›‹/failure› + + ]]›‹/failure› ‹/testcase› ‹testcase name="code in multiple nodes "after all" hook" classname="X-Pack Mocha Tests.x-pack/legacy/plugins/code/server/__tests__/multi_node·ts" time="0.003"› ‹system-out› @@ -324,11 +325,11 @@ it('rewrites karma reports with minimal changes', async () => { + at Generator.prototype.‹computed› [as next] (webpack://%5Bname%5D/./node_modules/regenerator-runtime/runtime.js?:114:21) + at asyncGeneratorStep (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158772:103) + at _next (http://localhost:5610/bundles/tests.bundle.js?shards=4&shard_num=1:158774:194) - + - + - +Failed Tests Reporter: - + - foo bar +]]›‹/failure› + + ‹system-out›Failed Tests Reporter: + + - foo bar + + + +‹/system-out› ‹/testcase› ‹testcase name="CoordinateMapsVisualizationTest CoordinateMapsVisualization - basics should toggle to Heatmap OK" time="0.055" classname="Browser Unit Tests.CoordinateMapsVisualizationTest"/› ‹testcase name="VegaParser._parseSchema should warn on vega-lite version too new to be supported" time="0.001" classname="Browser Unit Tests.VegaParser·_parseSchema"/› diff --git a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts index f82e1ef1fc19a9..32ea5fa0f90339 100644 --- a/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/add_messages_to_report.ts @@ -57,16 +57,14 @@ export async function addMessagesToReport(options: { } log.info(`${classname} - ${name}:${messageList}`); - const append = `\n\nFailed Tests Reporter:${messageList}\n`; + const output = `Failed Tests Reporter:${messageList}\n\n`; - if ( - testCase.failure[0] && - typeof testCase.failure[0] === 'object' && - typeof testCase.failure[0]._ === 'string' - ) { - testCase.failure[0]._ += append; + if (!testCase['system-out']) { + testCase['system-out'] = [output]; + } else if (typeof testCase['system-out'][0] === 'string') { + testCase['system-out'][0] = output + String(testCase['system-out'][0]); } else { - testCase.failure[0] = String(testCase.failure[0]) + append; + testCase['system-out'][0]._ = output + testCase['system-out'][0]._; } } diff --git a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts index fe6e0bbc796eed..23d9805727f324 100644 --- a/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts +++ b/packages/kbn-test/src/failed_tests_reporter/get_failures.test.ts @@ -48,6 +48,7 @@ it('discovers failures in ftr report', async () => { at process._tickCallback (internal/process/next_tick.js:68:7) name: 'NoSuchSessionError', remoteStacktrace: '' } ", "likelyIrrelevant": true, + "metadata-json": "{\\"messages\\":[\\"foo\\"],\\"screenshots\\":[{\\"name\\":\\"failure[dashboard app using current data dashboard snapshots compare TSVB snapshot]\\",\\"url\\":\\"https://storage.googleapis.com/kibana-ci-artifacts/jobs/elastic+kibana+7.x/1632/kibana-oss-tests/test/functional/screenshots/failure/dashboard%20app%20using%20current%20data%20dashboard%20snapshots%20compare%20TSVB%20snapshot.png\\"}]}", "name": "maps app \\"after all\\" hook", "time": "0.179", }, diff --git a/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts b/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts new file mode 100644 index 00000000000000..729d80ddfcb449 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/report_metadata.test.ts @@ -0,0 +1,49 @@ +/* + * 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 { getReportMessageIter } from './report_metadata'; +import { parseTestReport } from './test_report'; +import { FTR_REPORT, JEST_REPORT, KARMA_REPORT, MOCHA_REPORT } from './__fixtures__'; + +it('reads messages and screenshots from metadata-json properties', async () => { + const ftrReport = await parseTestReport(FTR_REPORT); + expect(Array.from(getReportMessageIter(ftrReport))).toMatchInlineSnapshot(` + Array [ + Object { + "classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps", + "message": "foo", + "name": "maps app \\"after all\\" hook", + }, + Object { + "classname": "Chrome X-Pack UI Functional Tests.x-pack/test/functional/apps/maps", + "message": "Screenshot: failure[dashboard app using current data dashboard snapshots compare TSVB snapshot] https://storage.googleapis.com/kibana-ci-artifacts/jobs/elastic+kibana+7.x/1632/kibana-oss-tests/test/functional/screenshots/failure/dashboard%20app%20using%20current%20data%20dashboard%20snapshots%20compare%20TSVB%20snapshot.png", + "name": "maps app \\"after all\\" hook", + }, + ] + `); + + const jestReport = await parseTestReport(JEST_REPORT); + expect(Array.from(getReportMessageIter(jestReport))).toMatchInlineSnapshot(`Array []`); + + const mochaReport = await parseTestReport(MOCHA_REPORT); + expect(Array.from(getReportMessageIter(mochaReport))).toMatchInlineSnapshot(`Array []`); + + const karmaReport = await parseTestReport(KARMA_REPORT); + expect(Array.from(getReportMessageIter(karmaReport))).toMatchInlineSnapshot(`Array []`); +}); diff --git a/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts b/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts new file mode 100644 index 00000000000000..5484948e599760 --- /dev/null +++ b/packages/kbn-test/src/failed_tests_reporter/report_metadata.ts @@ -0,0 +1,42 @@ +/* + * 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 { TestReport, makeTestCaseIter } from './test_report'; + +export function* getReportMessageIter(report: TestReport) { + for (const testCase of makeTestCaseIter(report)) { + const metadata = testCase.$['metadata-json'] ? JSON.parse(testCase.$['metadata-json']) : {}; + + for (const message of metadata.messages || []) { + yield { + classname: testCase.$.classname, + name: testCase.$.name, + message: String(message), + }; + } + + for (const screenshot of metadata.screenshots || []) { + yield { + classname: testCase.$.classname, + name: testCase.$.name, + message: `Screenshot: ${screenshot.name} ${screenshot.url}`, + }; + } + } +} diff --git a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts index b3c2a8dc338da9..fc52fa6cbf9e7d 100644 --- a/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts +++ b/packages/kbn-test/src/failed_tests_reporter/run_failed_tests_reporter_cli.ts @@ -25,7 +25,8 @@ import { GithubApi } from './github_api'; import { updateFailureIssue, createFailureIssue } from './report_failure'; import { getIssueMetadata } from './issue_metadata'; import { readTestReport } from './test_report'; -import { addMessagesToReport, Message } from './add_messages_to_report'; +import { addMessagesToReport } from './add_messages_to_report'; +import { getReportMessageIter } from './report_metadata'; export function runFailedTestsReporterCli() { run( @@ -74,17 +75,22 @@ export function runFailedTestsReporterCli() { for (const reportPath of reportPaths) { const report = await readTestReport(reportPath); - const messages: Message[] = []; + const messages = Array.from(getReportMessageIter(report)); for (const failure of await getFailures(report)) { - if (failure.likelyIrrelevant) { + const pushMessage = (msg: string) => { messages.push({ classname: failure.classname, name: failure.name, - message: - 'Failure is likely irrelevant' + - (updateGithub ? ', so an issue was not created or updated' : ''), + message: msg, }); + }; + + if (failure.likelyIrrelevant) { + pushMessage( + 'Failure is likely irrelevant' + + (updateGithub ? ', so an issue was not created or updated' : '') + ); continue; } @@ -97,30 +103,18 @@ export function runFailedTestsReporterCli() { if (existingIssue) { const newFailureCount = await updateFailureIssue(buildUrl, existingIssue, githubApi); const url = existingIssue.html_url; - const message = - `Test has failed ${newFailureCount - 1} times on tracked branches: ${url}` + - (updateGithub - ? `. Updated existing issue: ${url} (fail count: ${newFailureCount})` - : ''); - - messages.push({ - classname: failure.classname, - name: failure.name, - message, - }); + pushMessage(`Test has failed ${newFailureCount - 1} times on tracked branches: ${url}`); + if (updateGithub) { + pushMessage(`Updated existing issue: ${url} (fail count: ${newFailureCount})`); + } continue; } const newIssueUrl = await createFailureIssue(buildUrl, failure, githubApi); - const message = - `Test has not failed recently on tracked branches` + - (updateGithub ? `Created new issue: ${newIssueUrl}` : ''); - - messages.push({ - classname: failure.classname, - name: failure.name, - message, - }); + pushMessage('Test has not failed recently on tracked branches'); + if (updateGithub) { + pushMessage(`Created new issue: ${newIssueUrl}`); + } } // mutates report to include messages and writes updated report to disk diff --git a/packages/kbn-test/src/failed_tests_reporter/test_report.ts b/packages/kbn-test/src/failed_tests_reporter/test_report.ts index 644a4cc9fd5a73..6b759ef1d4c626 100644 --- a/packages/kbn-test/src/failed_tests_reporter/test_report.ts +++ b/packages/kbn-test/src/failed_tests_reporter/test_report.ts @@ -58,13 +58,15 @@ export interface TestCase { classname: string; /* number of seconds this test took */ time: string; + /* optional JSON encoded metadata */ + 'metadata-json'?: string; }; /* contents of system-out elements */ - 'system-out'?: string[]; + 'system-out'?: Array; /* contents of failure elements */ failure?: Array; /* contents of skipped elements */ - skipped?: string[]; + skipped?: Array; } export interface FailedTestCase extends TestCase { @@ -82,19 +84,23 @@ export async function readTestReport(testReportPath: string) { return await parseTestReport(await readAsync(testReportPath, 'utf8')); } -export function* makeFailedTestCaseIter(report: TestReport) { - // Grab the failures. Reporters may report multiple testsuites in a single file. +export function* makeTestCaseIter(report: TestReport) { + // Reporters may report multiple testsuites in a single file. const testSuites = 'testsuites' in report ? report.testsuites.testsuite : [report.testsuite]; for (const testSuite of testSuites) { for (const testCase of testSuite.testcase) { - const { failure } = testCase; - - if (!failure) { - continue; - } + yield testCase; + } + } +} - yield testCase as FailedTestCase; +export function* makeFailedTestCaseIter(report: TestReport) { + for (const testCase of makeTestCaseIter(report)) { + if (!testCase.failure) { + continue; } + + yield testCase as FailedTestCase; } } diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 36412961ce75b4..11b9450f2af6eb 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -57,6 +57,14 @@ export function runFtrCli() { } ); + if (flags.throttle) { + process.env.TEST_THROTTLE_NETWORK = '1'; + } + + if (flags.headless) { + process.env.TEST_BROWSER_HEADLESS = '1'; + } + let teardownRun = false; const teardown = async (err?: Error) => { if (teardownRun) return; @@ -97,7 +105,7 @@ export function runFtrCli() { { flags: { string: ['config', 'grep', 'exclude', 'include-tag', 'exclude-tag', 'kibana-install-dir'], - boolean: ['bail', 'invert', 'test-stats', 'updateBaselines'], + boolean: ['bail', 'invert', 'test-stats', 'updateBaselines', 'throttle', 'headless'], default: { config: 'test/functional/config.js', debug: true, @@ -113,6 +121,8 @@ export function runFtrCli() { --test-stats print the number of tests (included and excluded) to STDERR --updateBaselines replace baseline screenshots with whatever is generated from the test --kibana-install-dir directory where the Kibana install being tested resides + --throttle enable network throttling in Chrome browser + --headless run browser in headless mode `, }, } diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index fcba9691b1772f..e566a9a4af2628 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -23,6 +23,7 @@ import { Suite, Test } from './fake_mocha_types'; import { Lifecycle, LifecyclePhase, + FailureMetadata, readConfigFile, ProviderCollection, readProviderSpec, @@ -33,6 +34,7 @@ import { export class FunctionalTestRunner { public readonly lifecycle = new Lifecycle(); + public readonly failureMetadata = new FailureMetadata(this.lifecycle); private closed = false; constructor( @@ -114,6 +116,7 @@ export class FunctionalTestRunner { const coreProviders = readProviderSpec('Service', { lifecycle: () => this.lifecycle, log: () => this.log, + failureMetadata: () => this.failureMetadata, config: () => config, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts new file mode 100644 index 00000000000000..7ae46ef6fac1e0 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Lifecycle } from './lifecycle'; +import { FailureMetadata } from './failure_metadata'; + +it('collects metadata for the current test', async () => { + const lifecycle = new Lifecycle(); + const failureMetadata = new FailureMetadata(lifecycle); + + const test1 = {}; + await lifecycle.beforeEachTest.trigger(test1); + failureMetadata.add({ foo: 'bar' }); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); + + const test2 = {}; + await lifecycle.beforeEachTest.trigger(test2); + failureMetadata.add({ test: 2 }); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "foo": "bar", + } + `); + expect(failureMetadata.get(test2)).toMatchInlineSnapshot(` + Object { + "test": 2, + } + `); +}); + +it('adds messages to the messages state', () => { + const lifecycle = new Lifecycle(); + const failureMetadata = new FailureMetadata(lifecycle); + + const test1 = {}; + lifecycle.beforeEachTest.trigger(test1); + failureMetadata.addMessages(['foo', 'bar']); + failureMetadata.addMessages(['baz']); + + expect(failureMetadata.get(test1)).toMatchInlineSnapshot(` + Object { + "messages": Array [ + "foo", + "bar", + "baz", + ], + } + `); +}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts new file mode 100644 index 00000000000000..9dc58d5b0b21f8 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Path from 'path'; + +import { REPO_ROOT } from '@kbn/dev-utils'; + +import { Lifecycle } from './lifecycle'; + +interface Metadata { + [key: string]: unknown; +} + +export class FailureMetadata { + // mocha's global types mean we can't import Mocha or it will override the global jest types.............. + private currentTest?: any; + private readonly allMetadata = new Map(); + + constructor(lifecycle: Lifecycle) { + if (!process.env.GCS_UPLOAD_PREFIX && process.env.CI) { + throw new Error( + 'GCS_UPLOAD_PREFIX environment variable is not set and must always be set on CI' + ); + } + + lifecycle.beforeEachTest.add(test => { + this.currentTest = test; + }); + } + + add(metadata: Metadata | ((current: Metadata) => Metadata)) { + if (!this.currentTest) { + throw new Error('no current test to associate metadata with'); + } + + const current = this.allMetadata.get(this.currentTest); + this.allMetadata.set(this.currentTest, { + ...current, + ...(typeof metadata === 'function' ? metadata(current || {}) : metadata), + }); + } + + addMessages(messages: string[]) { + this.add(current => ({ + messages: [...(Array.isArray(current.messages) ? current.messages : []), ...messages], + })); + } + + /** + * @param name Name to label the URL with + * @param repoPath absolute path, within the repo, that will be uploaded + */ + addScreenshot(name: string, repoPath: string) { + const prefix = process.env.GCS_UPLOAD_PREFIX; + + if (!prefix) { + return; + } + + const slash = prefix.endsWith('/') ? '' : '/'; + const urlPath = Path.relative(REPO_ROOT, repoPath) + .split(Path.sep) + .map(c => encodeURIComponent(c)) + .join('/'); + + if (urlPath.startsWith('..')) { + throw new Error( + `Only call addUploadLink() with paths that are within the repo root, received ${repoPath} and repo root is ${REPO_ROOT}` + ); + } + + const url = `https://storage.googleapis.com/${prefix}${slash}${urlPath}`; + const screenshot = { + name, + url, + }; + + this.add(current => ({ + screenshots: [...(Array.isArray(current.screenshots) ? current.screenshots : []), screenshot], + })); + + return screenshot; + } + + get(test: any) { + return this.allMetadata.get(test); + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 2d354938d76483..8940eccad503a4 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -22,3 +22,4 @@ export { LifecyclePhase } from './lifecycle_phase'; export { readConfigFile, Config } from './config'; export { readProviderSpec, ProviderCollection, Provider } from './providers'; export { runTests, setupMocha } from './mocha'; +export { FailureMetadata } from './failure_metadata'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.ts new file mode 100644 index 00000000000000..22b73634543612 --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle_event.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 * as Rx from 'rxjs'; + +export type GetArgsType> = T extends LifecycleEvent + ? X + : never; + +export class LifecycleEvent { + private readonly handlers: Array<(...args: Args) => Promise | void> = []; + + private readonly beforeSubj = this.options.singular + ? new Rx.BehaviorSubject(undefined) + : new Rx.Subject(); + public readonly before$ = this.beforeSubj.asObservable(); + + private readonly afterSubj = this.options.singular + ? new Rx.BehaviorSubject(undefined) + : new Rx.Subject(); + public readonly after$ = this.afterSubj.asObservable(); + + constructor( + private readonly options: { + singular?: boolean; + } = {} + ) {} + + public add(fn: (...args: Args) => Promise | void) { + this.handlers.push(fn); + } + + public async trigger(...args: Args) { + if (this.beforeSubj.isStopped) { + throw new Error(`singular lifecycle event can only be triggered once`); + } + + this.beforeSubj.next(undefined); + if (this.options.singular) { + this.beforeSubj.complete(); + } + + try { + await Promise.all(this.handlers.map(async fn => await fn(...args))); + } finally { + this.afterSubj.next(undefined); + if (this.options.singular) { + this.afterSubj.complete(); + } + } + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js index ea697b096ce99f..0e8c1bc121e155 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js @@ -32,6 +32,7 @@ import { writeEpilogue } from './write_epilogue'; export function MochaReporterProvider({ getService }) { const log = getService('log'); const config = getService('config'); + const failureMetadata = getService('failureMetadata'); let originalLogWriters; let reporterCaptureStartTime; @@ -53,6 +54,7 @@ export function MochaReporterProvider({ getService }) { if (config.get('junit.enabled') && config.get('junit.reportName')) { setupJUnitReportGeneration(runner, { reportName: config.get('junit.reportName'), + getTestMetadata: t => failureMetadata.get(t), }); } } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js index 97b74a3b2b5412..9f9a8f59fde9ad 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/cli.test.js @@ -182,6 +182,22 @@ describe('run tests CLI', () => { expect(exitMock).not.toHaveBeenCalled(); }); + it('accepts network throttle option', async () => { + global.process.argv.push('--throttle'); + + await runTestsCli(['foo']); + + expect(exitMock).toHaveBeenCalledWith(1); + }); + + it('accepts headless option', async () => { + global.process.argv.push('--headless'); + + await runTestsCli(['foo']); + + expect(exitMock).toHaveBeenCalledWith(1); + }); + it('accepts extra server options', async () => { global.process.argv.push('--', '--server.foo=bar'); diff --git a/packages/kbn-test/src/index.ts b/packages/kbn-test/src/index.ts index 62739fd37030f1..06a83cdd8b1dcb 100644 --- a/packages/kbn-test/src/index.ts +++ b/packages/kbn-test/src/index.ts @@ -49,3 +49,5 @@ export { } from './mocha'; export { runFailedTestsReporterCli } from './failed_tests_reporter'; + +export { makeJunitReportPath } from './junit_report_path'; diff --git a/packages/kbn-test/src/junit_report_path.ts b/packages/kbn-test/src/junit_report_path.ts new file mode 100644 index 00000000000000..11eaf3d2b14a5e --- /dev/null +++ b/packages/kbn-test/src/junit_report_path.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; + +const job = process.env.JOB ? `job-${process.env.JOB}-` : ''; +const num = process.env.CI_WORKER_NUMBER ? `worker-${process.env.CI_WORKER_NUMBER}-` : ''; + +export function makeJunitReportPath(rootDirectory: string, reportName: string) { + return resolve( + rootDirectory, + 'target/junit', + process.env.JOB || '.', + `TEST-${job}${num}${reportName}.xml` + ); +} diff --git a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js index 1cb53ea0ca1c5e..7472e271bd1e9c 100644 --- a/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/__tests__/junit_report_generation.js @@ -25,6 +25,7 @@ import { parseString } from 'xml2js'; import del from 'del'; import Mocha from 'mocha'; import expect from '@kbn/expect'; +import { makeJunitReportPath } from '@kbn/test'; import { setupJUnitReportGeneration } from '../junit_report_generation'; @@ -50,17 +51,7 @@ describe('dev/mocha/junit report generation', () => { mocha.addFile(resolve(PROJECT_DIR, 'test.js')); await new Promise(resolve => mocha.run(resolve)); const report = await fcb(cb => - parseString( - readFileSync( - resolve( - PROJECT_DIR, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}test.xml` - ) - ), - cb - ) + parseString(readFileSync(makeJunitReportPath(PROJECT_DIR, 'test')), cb) ); // test case results are wrapped in @@ -98,6 +89,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE works', time: testPass.$.time, + 'metadata-json': '{}', }, 'system-out': testPass['system-out'], }); @@ -109,6 +101,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE fails', time: testFail.$.time, + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], failure: [testFail.failure[0]], @@ -124,6 +117,7 @@ describe('dev/mocha/junit report generation', () => { classname: sharedClassname, name: 'SUITE SUB_SUITE "before each" hook: fail hook for "never runs"', time: beforeEachFail.$.time, + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], failure: [beforeEachFail.failure[0]], @@ -133,6 +127,7 @@ describe('dev/mocha/junit report generation', () => { $: { classname: sharedClassname, name: 'SUITE SUB_SUITE never runs', + 'metadata-json': '{}', }, 'system-out': testFail['system-out'], skipped: [''], diff --git a/packages/kbn-test/src/mocha/junit_report_generation.js b/packages/kbn-test/src/mocha/junit_report_generation.js index 51601fab12e531..95e84117106a4d 100644 --- a/packages/kbn-test/src/mocha/junit_report_generation.js +++ b/packages/kbn-test/src/mocha/junit_report_generation.js @@ -17,11 +17,12 @@ * under the License. */ -import { resolve, dirname, relative } from 'path'; +import { dirname, relative } from 'path'; import { writeFileSync, mkdirSync } from 'fs'; import { inspect } from 'util'; import xmlBuilder from 'xmlbuilder'; +import { makeJunitReportPath } from '@kbn/test'; import { getSnapshotOfRunnableLogs } from './log_cache'; import { escapeCdata } from '../'; @@ -32,6 +33,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { const { reportName = 'Unnamed Mocha Tests', rootDirectory = dirname(require.resolve('../../../../package.json')), + getTestMetadata = () => ({}), } = options; const stats = {}; @@ -118,6 +120,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { name: getFullTitle(node), classname: `${reportName}.${getPath(node).replace(/\./g, '·')}`, time: getDuration(node), + 'metadata-json': JSON.stringify(getTestMetadata(node) || {}), }); } @@ -135,13 +138,7 @@ export function setupJUnitReportGeneration(runner, options = {}) { } }); - const reportPath = resolve( - rootDirectory, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml` - ); - + const reportPath = makeJunitReportPath(rootDirectory, reportName); const reportXML = builder.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/packages/kbn-test/types/ftr.d.ts b/packages/kbn-test/types/ftr.d.ts index e917ed63ca5d30..8beecab88878da 100644 --- a/packages/kbn-test/types/ftr.d.ts +++ b/packages/kbn-test/types/ftr.d.ts @@ -18,9 +18,9 @@ */ import { ToolingLog } from '@kbn/dev-utils'; -import { Config, Lifecycle } from '../src/functional_test_runner/lib'; +import { Config, Lifecycle, FailureMetadata } from '../src/functional_test_runner/lib'; -export { Lifecycle, Config }; +export { Lifecycle, Config, FailureMetadata }; interface AsyncInstance { /** @@ -61,7 +61,7 @@ export interface GenericFtrProviderContext< * Determine if a service is avaliable * @param serviceName */ - hasService(serviceName: 'config' | 'log' | 'lifecycle'): true; + hasService(serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata'): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -73,6 +73,7 @@ export interface GenericFtrProviderContext< getService(serviceName: 'config'): Config; getService(serviceName: 'log'): ToolingLog; getService(serviceName: 'lifecycle'): Lifecycle; + getService(serviceName: 'failureMetadata'): FailureMetadata; getService(serviceName: T): ServiceMap[T]; /** diff --git a/renovate.json5 b/renovate.json5 index 3886715618e99b..49474b28e79088 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -13,6 +13,7 @@ 'x-pack/package.json', 'x-pack/legacy/plugins/*/package.json', 'packages/*/package.json', + 'examples/*/package.json', 'test/plugin_functional/plugins/*/package.json', 'test/interpreter_functional/plugins/*/package.json', ], @@ -953,5 +954,6 @@ enabled: false, }, rebaseStalePrs: false, + rebaseConflictedPrs: false, semanticCommits: false, } diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index b65cd3835cc0ae..2526e2b14e9a55 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -24,4 +24,5 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/plugin_functional/config.js'), require.resolve('../test/interpreter_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), + require.resolve('../test/examples/config.js') ]); diff --git a/src/apm.js b/src/apm.js index 04a70ee71c53ec..5effefaccd0296 100644 --- a/src/apm.js +++ b/src/apm.js @@ -25,15 +25,13 @@ module.exports = function (serviceName = name) { if (process.env.kbnWorkerType === 'optmzr') return; const conf = { - serviceName: `${serviceName}-${version.replace(/\./g, '_')}` + serviceName: `${serviceName}-${version.replace(/\./g, '_')}`, }; - if (configFileExists()) conf.configFile = 'config/apm.js'; + const configFile = join(__dirname, '..', 'config', 'apm.js'); + + if (existsSync(configFile)) conf.configFile = configFile; else conf.active = false; require('elastic-apm-node').start(conf); }; - -function configFileExists() { - return existsSync(join(__dirname, '..', 'config', 'apm.js')); -} diff --git a/src/cli/cluster/cluster_manager.js b/src/cli/cluster/cluster_manager.js index 050d13b4b2c3e8..f12a161fe2246e 100644 --- a/src/cli/cluster/cluster_manager.js +++ b/src/cli/cluster/cluster_manager.js @@ -29,7 +29,6 @@ import { REPO_ROOT } from '@kbn/dev-utils'; import Log from '../log'; import Worker from './worker'; import { Config } from '../../legacy/server/config/config'; -import { transformDeprecations } from '../../legacy/server/config/transform_deprecations'; process.env.kbnWorkerType = 'managr'; @@ -37,7 +36,7 @@ export default class ClusterManager { static create(opts, settings = {}, basePathProxy) { return new ClusterManager( opts, - Config.withDefaultSchema(transformDeprecations(settings)), + Config.withDefaultSchema(settings), basePathProxy ); } diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 48b5db318d1c2d..fd0502e07e4221 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -144,6 +144,11 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { set('plugins.paths', _.compact([].concat( get('plugins.paths'), opts.pluginPath, + opts.runExamples ? [ + // Ideally this would automatically include all plugins in the examples dir + fromRoot('examples/demo_search'), + fromRoot('examples/search_explorer'), + ] : [], XPACK_INSTALLED && !opts.oss ? [XPACK_DIR] @@ -201,7 +206,8 @@ export default function (program) { if (!IS_KIBANA_DISTRIBUTABLE) { command - .option('--oss', 'Start Kibana without X-Pack'); + .option('--oss', 'Start Kibana without X-Pack') + .option('--run-examples', 'Adds plugin paths for all the Kibana example plugins and runs with no base path'); } if (CAN_CLUSTER) { @@ -238,7 +244,12 @@ export default function (program) { silent: !!opts.silent, watch: !!opts.watch, repl: !!opts.repl, - basePath: !!opts.basePath, + // We want to run without base path when the `--run-examples` flag is given so that we can use local + // links in other documentation sources, like "View this tutorial [here](http://localhost:5601/app/tutorial/xyz)". + // We can tell users they only have to run with `yarn start --run-examples` to get those + // local links to work. Similar to what we do for "View in Console" links in our + // elastic.co links. + basePath: opts.runExamples ? false : !!opts.basePath, optimize: !!opts.optimize, oss: !!opts.oss }, diff --git a/src/cli_keystore/cli_keystore.js b/src/cli_keystore/cli_keystore.js index 47b49d936028e3..d20d55f23d83c9 100644 --- a/src/cli_keystore/cli_keystore.js +++ b/src/cli_keystore/cli_keystore.js @@ -17,6 +17,7 @@ * under the License. */ +import _ from 'lodash'; import { join } from 'path'; import { pkg } from '../core/server/utils'; @@ -32,6 +33,7 @@ import { listCli } from './list'; import { addCli } from './add'; import { removeCli } from './remove'; +const argv = process.env.kbnWorkerArgv ? JSON.parse(process.env.kbnWorkerArgv) : process.argv.slice(); const program = new Command('bin/kibana-keystore'); program @@ -43,8 +45,25 @@ listCli(program, keystore); addCli(program, keystore); removeCli(program, keystore); -program.parse(process.argv); +program + .command('help ') + .description('get the help for a specific command') + .action(function (cmdName) { + const cmd = _.find(program.commands, { _name: cmdName }); + if (!cmd) return program.error(`unknown command ${cmdName}`); + cmd.help(); + }); + +program + .command('*', null, { noHelp: true }) + .action(function (cmd) { + program.error(`unknown command ${cmd}`); + }); -if (!program.args.length) { - program.help(); +// check for no command name +const subCommand = argv[2] && !String(argv[2][0]).match(/^-|^\.|\//); +if (!subCommand) { + program.defaultHelp(); } + +program.parse(process.argv); diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 786409614e6d94..18f82766bdbc16 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -130,16 +130,16 @@ leverage this pattern. import React from 'react'; import ReactDOM from 'react-dom'; -import { ApplicationMountContext } from '../../src/core/public'; +import { CoreStart, AppMountParams } from '../../src/core/public'; import { MyAppRoot } from './components/app.ts'; /** * This module will be loaded asynchronously to reduce the bundle size of your plugin's main bundle. */ -export const renderApp = (context: ApplicationMountContext, domElement: HTMLDivElement) => { - ReactDOM.render(, domElement); - return () => ReactDOM.unmountComponentAtNode(domElement); +export const renderApp = (core: CoreStart, deps: MyPluginDepsStart, { element, appBasePath }: AppMountParams) => { + ReactDOM.render(, element); + return () => ReactDOM.unmountComponentAtNode(element); } ``` @@ -152,10 +152,12 @@ export class MyPlugin implements Plugin { public setup(core) { core.application.register({ id: 'my-app', - async mount(context, domElement) { + async mount(params) { // Load application bundle const { renderApp } = await import('./application/my_app'); - return renderApp(context, domElement); + // Get start services + const [coreStart, depsStart] = core.getStartServices(); + return renderApp(coreStart, depsStart, params); } }); } diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index 2527ffba2cbbd5..7c3fa4afad2ae8 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -9,22 +9,21 @@ - [Challenges on the server](#challenges-on-the-server) - [Challenges in the browser](#challenges-in-the-browser) - [Plan of action](#plan-of-action) - - [Shared application plugins](#shared-application-plugins) - [Server-side plan of action](#server-side-plan-of-action) - [De-couple from hapi.js server and request objects](#de-couple-from-hapijs-server-and-request-objects) - [Introduce new plugin definition shim](#introduce-new-plugin-definition-shim) - [Switch to new platform services](#switch-to-new-platform-services) - [Migrate to the new plugin system](#migrate-to-the-new-plugin-system) - [Browser-side plan of action](#browser-side-plan-of-action) - - [1. Create a plugin definition file](#1-create-a-plugin-definition-file) - - [2. Export all static code and types from `public/index.ts`](#2-export-all-static-code-and-types-from-publicindexts) - - [3. Export your runtime contract](#3-export-your-runtime-contract) - - [4. Move "owned" UI modules into your plugin and expose them from your public contract](#4-move-owned-ui-modules-into-your-plugin-and-expose-them-from-your-public-contract) - - [5. Provide plugin extension points decoupled from angular.js](#5-provide-plugin-extension-points-decoupled-from-angularjs) - - [6. Move all webpack alias imports into uiExport entry files](#6-move-all-webpack-alias-imports-into-uiexport-entry-files) - - [7. Switch to new platform services](#7-switch-to-new-platform-services) - - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) - - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) + - [1. Create a plugin definition file](#1-create-a-plugin-definition-file) + - [2. Export all static code and types from `public/index.ts`](#2-export-all-static-code-and-types-from-publicindexts) + - [3. Export your runtime contract](#3-export-your-runtime-contract) + - [4. Move "owned" UI modules into your plugin and expose them from your public contract](#4-move-owned-ui-modules-into-your-plugin-and-expose-them-from-your-public-contract) + - [5. Provide plugin extension points decoupled from angular.js](#5-provide-plugin-extension-points-decoupled-from-angularjs) + - [6. Move all webpack alias imports into uiExport entry files](#6-move-all-webpack-alias-imports-into-uiexport-entry-files) + - [7. Switch to new platform services](#7-switch-to-new-platform-services) + - [8. Migrate to the new plugin system](#8-migrate-to-the-new-plugin-system) + - [Bonus: Tips for complex migration scenarios](#bonus-tips-for-complex-migration-scenarios) - [Frequently asked questions](#frequently-asked-questions) - [Is migrating a plugin an all-or-nothing thing?](#is-migrating-a-plugin-an-all-or-nothing-thing) - [Do plugins need to be converted to TypeScript?](#do-plugins-need-to-be-converted-to-typescript) @@ -42,9 +41,11 @@ - [Plugins for shared application services](#plugins-for-shared-application-services) - [Server-side](#server-side) - [Core services](#core-services-1) + - [Plugin services](#plugin-services) - [UI Exports](#ui-exports) - [How to](#how-to) - [Configure plugin](#configure-plugin) + - [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) - [Mock new platform services in tests](#mock-new-platform-services-in-tests) - [Writing mocks for your plugin](#writing-mocks-for-your-plugin) - [Using mocks in your tests](#using-mocks-in-your-tests) @@ -325,43 +326,6 @@ First, decouple your plugin's business logic from the dependencies that are not Once those things are finished for any given plugin, it can officially be switched to the new plugin system. -### Shared application plugins - -Some services have been already moved to the new platform. - -Below you can find their new locations: - -| Service | Old place | New place in the NP | -| --------------- | ----------------------------------------- | --------------------------------------------------- | -| *FieldFormats* | ui/registry/field_formats | plugins/data/public | - -The `FieldFormats` service has been moved to the `data` plugin in the New Platform. If your plugin has any imports from `ui/registry/field_formats`, you'll need to update your imports as follows: - -Use it in your New Platform plugin: - -```ts -class MyPlugin { - setup (core, { data }) { - data.fieldFormats.register(myFieldFormat); - // ... - } - start (core, { data }) { - data.fieldFormats.getType(myFieldFormatId); - // ... - } -} -``` - -Or, in your legacy platform plugin, consume it through the `ui/new_platform` module: - -```ts -import { npSetup, npStart } from 'ui/new_platform'; - -npSetup.plugins.data.fieldFormats.register(myFieldFormat); -npStart.plugins.data.fieldFormats.getType(myFieldFormatId); -// ... -``` - ## Server-side plan of action Legacy server-side plugins access functionality from core and other plugins at runtime via function arguments, which is similar to how they must be architected to use the new plugin system. This greatly simplifies the plan of action for migrating server-side plugins. @@ -1191,25 +1155,26 @@ import { setup, start } from '../core_plugins/embeddables/public/legacy'; import { setup, start } from '../core_plugins/visualizations/public/legacy'; ``` -| Legacy Platform | New Platform | Notes | -| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `import 'ui/apply_filters'` | N/A. Replaced by triggering an APPLY_FILTER_TRIGGER trigger. | Directive is deprecated. | -| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | -| `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | -| `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | -| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | -| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | -| `core_plugins/interpreter` | `data.expressions` | still in progress | -| `ui/courier` | `data.search` | still in progress | -| `ui/embeddable` | `embeddables` | still in progress | -| `ui/filter_manager` | `data.filter` | -- | -| `ui/index_patterns` | `data.indexPatterns` | still in progress | -| `ui/registry/feature_catalogue` | `home.featureCatalogue.register` | Must add `home` as a dependency in your kibana.json. | -| `ui/registry/vis_types` | `visualizations.types` | -- | -| `ui/vis` | `visualizations.types` | -- | -| `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` | -| `ui/vis/vis_factory` | `visualizations.types` | -- | -| `ui/vis/vis_filters` | `visualizations.filters` | -- | +| Legacy Platform | New Platform | Notes | +| ------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `import 'ui/apply_filters'` | N/A. Replaced by triggering an APPLY_FILTER_TRIGGER trigger. | Directive is deprecated. | +| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | +| `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | +| `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | +| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. | +| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | | +| `core_plugins/interpreter` | `data.expressions` | still in progress | +| `ui/courier` | `data.search` | still in progress | +| `ui/embeddable` | `embeddables` | still in progress | +| `ui/filter_manager` | `data.filter` | -- | +| `ui/index_patterns` | `data.indexPatterns` | still in progress | +| `ui/registry/field_formats` | `data.fieldFormats` | | +| `ui/registry/feature_catalogue` | `home.featureCatalogue.register` | Must add `home` as a dependency in your kibana.json. | +| `ui/registry/vis_types` | `visualizations.types` | -- | +| `ui/vis` | `visualizations.types` | -- | +| `ui/share` | `share` | `showShareContextMenu` is now called `toggleShareContextMenu`, `ShareContextMenuExtensionsRegistryProvider` is now called `register` | +| `ui/vis/vis_factory` | `visualizations.types` | -- | +| `ui/vis/vis_filters` | `visualizations.filters` | -- | | `ui/utils/parse_es_interval` | `import { parseEsInterval } from '../data/public'` | `parseEsInterval`, `ParsedInterval`, `InvalidEsCalendarIntervalError`, `InvalidEsIntervalFormatError` items were moved to the `Data Plugin` as a static code | #### Server-side @@ -1218,17 +1183,28 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy'; In server code, `core` can be accessed from either `server.newPlatform` or `kbnServer.newPlatform`. There are not currently very many services available on the server-side: -| Legacy Platform | New Platform | Notes | -| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | -| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | -| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | -| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | -| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | -| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | +| Legacy Platform | New Platform | Notes | +| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `server.config()` | [`initializerContext.config.create()`](/docs/development/core/server/kibana-plugin-server.plugininitializercontext.config.md) | Must also define schema. See _[how to configure plugin](#configure-plugin)_ | +| `server.route` | [`core.http.createRouter`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.createrouter.md) | [Examples](./MIGRATION_EXAMPLES.md#route-registration) | +| `request.getBasePath()` | [`core.http.basePath.get`](/docs/development/core/server/kibana-plugin-server.httpservicesetup.basepath.md) | | +| `server.plugins.elasticsearch.getCluster('data')` | [`core.elasticsearch.dataClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.dataclient_.md) | Handlers will also include a pre-configured client | +| `server.plugins.elasticsearch.getCluster('admin')` | [`core.elasticsearch.adminClient$`](/docs/development/core/server/kibana-plugin-server.elasticsearchservicesetup.adminclient_.md) | Handlers will also include a pre-configured client | +| `xpackMainPlugin.info.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | +| `server.savedObjects.setScopedSavedObjectsClientFactory` | [`core.savedObjects.setClientFactory`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.setclientfactory.md) | | +| `server.savedObjects.addScopedSavedObjectsClientWrapperFactory` | [`core.savedObjects.addClientWrapper`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.addclientwrapper.md) | | +| `server.savedObjects.getSavedObjectsRepository` | [`core.savedObjects.createInternalRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createinternalrepository.md) [`core.savedObjects.createScopedRepository`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicesetup.createscopedrepository.md) | | +| `server.savedObjects.getScopedSavedObjectsClient` | [`core.savedObjects.getScopedClient`](/docs/development/core/server/kibana-plugin-server.savedobjectsservicestart.getscopedclient.md) | | +| `request.getSavedObjectsClient` | [`context.core.savedObjects.client`](/docs/development/core/server/kibana-plugin-server.requesthandlercontext.core.md) | | +| `kibana.Plugin.deprecations` | [Handle plugin configuration deprecations](#handle-plugin-config-deprecations) and [`PluginConfigDescriptor.deprecations`](docs/development/core/server/kibana-plugin-server.pluginconfigdescriptor.md) | Deprecations from New Platform are not applied to legacy configuration | _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-plugin-server.coresetup.md)_ +##### Plugin services +| Legacy Platform | New Platform | Notes | +| ------------------------------------------- | ------------------------------------------------------------------------------ | ----- | +| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | + #### UI Exports The legacy platform uses a set of "uiExports" to inject modules from one plugin into other plugins. This mechansim is not necessary in the New Platform because all plugins are executed on the page at once (though only one application) is rendered at a time. @@ -1249,7 +1225,7 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `fieldFormatEditors` | | | | `fieldFormats` | | | | `hacks` | n/a | Just run the code in your plugin's `start` method. | -| `home` | [`plugins.home.featureCatalogue.register`](./src/plugins/home/public/feature_catalogue) | Must add `home` as a dependency in your kibana.json. | +| `home` | [`plugins.home.featureCatalogue.register`](./src/plugins/home/public/feature_catalogue) | Must add `home` as a dependency in your kibana.json. | | `indexManagement` | | Should be an API on the indexManagement plugin. | | `injectDefaultVars` | n/a | Plugins will only be able to "whitelist" config values for the frontend. See [#41990](https://github.com/elastic/kibana/issues/41990) | | `inspectorViews` | | Should be an API on the data (?) plugin. | @@ -1267,7 +1243,7 @@ This table shows where these uiExports have moved to in the New Platform. In mos | `styleSheetPaths` | | | | `taskDefinitions` | | Should be an API on the taskManager plugin. | | `uiCapabilities` | [`core.application.register`](/docs/development/core/public/kibana-plugin-public.applicationsetup.register.md) | | -| `uiSettingDefaults` | [`core.uiSettings.register`](/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md) | | +| `uiSettingDefaults` | [`core.uiSettings.register`](/docs/development/core/server/kibana-plugin-server.uisettingsservicesetup.md) | | | `validations` | | Part of SavedObjects, see [#33587](https://github.com/elastic/kibana/issues/33587) | | `visEditorTypes` | | | | `visTypeEnhancers` | | | @@ -1397,6 +1373,52 @@ export const config = { }; ``` +#### Handle plugin configuration deprecations + +If your plugin have deprecated properties, you can describe them using the `deprecations` config descriptor field. + +The system is quite similar to the legacy plugin's deprecation management. The most important difference +is that deprecations are managed on a per-plugin basis, meaning that you don't need to specify the whole +property path, but use the relative path from your plugin's configuration root. + +```typescript +// my_plugin/server/index.ts +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; + +const configSchema = schema.object({ + newProperty: schema.string({ defaultValue: 'Some string' }), +}); + +type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ rename, unused }) => [ + rename('oldProperty', 'newProperty'), + unused('someUnusedProperty'), + ] +}; +``` + +In some cases, accessing the whole configuration for deprecations is necessary. For these edge cases, +`renameFromRoot` and `unusedFromRoot` are also accessible when declaring deprecations. + +```typescript +// my_plugin/server/index.ts +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot, unusedFromRoot }) => [ + renameFromRoot('oldplugin.property', 'myplugin.property'), + unusedFromRoot('oldplugin.deprecated'), + ] +}; +``` + +Note that deprecations registered in new platform's plugins are not applied to the legacy configuration. +During migration, if you still need the deprecations to be effective in the legacy plugin, you need to declare them in +both plugin definitions. + ### Mock new platform services in tests #### Writing mocks for your plugin diff --git a/src/core/public/application/application_service.test.tsx b/src/core/public/application/application_service.test.tsx index 19a208aeefb372..32634572466a67 100644 --- a/src/core/public/application/application_service.test.tsx +++ b/src/core/public/application/application_service.test.tsx @@ -32,9 +32,9 @@ describe('#setup()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any); expect(() => - setup.register(Symbol(), { id: 'app1' } as any) + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any) ).toThrowErrorMatchingInlineSnapshot( `"An application is already registered with the id \\"app1\\""` ); @@ -51,6 +51,18 @@ describe('#setup()', () => { setup.register(Symbol(), { id: 'app1' } as any) ).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`); }); + + it('logs a warning when registering a deprecated app mount', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn'); + const service = new ApplicationService(); + const context = contextServiceMock.createSetupContract(); + const setup = service.setup({ context }); + setup.register(Symbol(), { id: 'app1', mount: (ctx: any, params: any) => {} } as any); + expect(consoleWarnSpy).toHaveBeenCalledWith( + `App [app1] is using deprecated mount context. Use core.getStartServices() instead.` + ); + consoleWarnSpy.mockRestore(); + }); }); describe('registerLegacyApp', () => { @@ -100,7 +112,7 @@ describe('#start()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + setup.register(Symbol(), { id: 'app1', mount: jest.fn() } as any); setup.registerLegacyApp({ id: 'app2' } as any); const http = httpServiceMock.createStartContract(); @@ -108,12 +120,13 @@ describe('#start()', () => { const startContract = await service.start({ http, injectedMetadata }); expect(startContract.availableApps).toMatchInlineSnapshot(` - Map { - "app1" => Object { - "id": "app1", - }, - } - `); + Map { + "app1" => Object { + "id": "app1", + "mount": [MockFunction], + }, + } + `); expect(startContract.availableLegacyApps).toMatchInlineSnapshot(` Map { "app2" => Object { @@ -127,14 +140,15 @@ describe('#start()', () => { const service = new ApplicationService(); const context = contextServiceMock.createSetupContract(); const setup = service.setup({ context }); - setup.register(Symbol(), { id: 'app1' } as any); + const app1 = { id: 'app1', mount: jest.fn() }; + setup.register(Symbol(), app1 as any); const http = httpServiceMock.createStartContract(); const injectedMetadata = injectedMetadataServiceMock.createStartContract(); await service.start({ http, injectedMetadata }); expect(MockCapabilitiesService.start).toHaveBeenCalledWith({ - apps: new Map([['app1', { id: 'app1' }]]), + apps: new Map([['app1', app1]]), legacyApps: new Map(), http, }); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 45ca7f3fe7f7b9..df00c84028e6f4 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -29,7 +29,8 @@ import { ContextSetup, IContextContainer } from '../context'; import { App, LegacyApp, - AppMounter, + AppMount, + AppMountDeprecated, InternalApplicationSetup, InternalApplicationStart, } from './types'; @@ -50,7 +51,7 @@ interface StartDeps { interface AppBox { app: App; - mount: AppMounter; + mount: AppMount; } /** @@ -61,7 +62,7 @@ export class ApplicationService { private readonly apps$ = new BehaviorSubject>(new Map()); private readonly legacyApps$ = new BehaviorSubject>(new Map()); private readonly capabilities = new CapabilitiesService(); - private mountContext?: IContextContainer; + private mountContext?: IContextContainer; public setup({ context }: SetupDeps): InternalApplicationSetup { this.mountContext = context.createContextContainer(); @@ -75,10 +76,21 @@ export class ApplicationService { throw new Error(`Applications cannot be registered after "setup"`); } - const appBox: AppBox = { - app, - mount: this.mountContext!.createHandler(plugin, app.mount), - }; + let appBox: AppBox; + if (isAppMountDeprecated(app.mount)) { + // eslint-disable-next-line no-console + console.warn( + `App [${app.id}] is using deprecated mount context. Use core.getStartServices() instead.` + ); + + appBox = { + app, + mount: this.mountContext!.createHandler(plugin, app.mount), + }; + } else { + appBox = { app, mount: app.mount }; + } + this.apps$.next(new Map([...this.apps$.value.entries(), [app.id, appBox]])); }, registerLegacyApp: (app: LegacyApp) => { @@ -146,7 +158,7 @@ export class ApplicationService { } // Filter only available apps and map to just the mount function. - const appMounters = new Map( + const appMounts = new Map( [...this.apps$.value] .filter(([id]) => availableApps.has(id)) .map(([id, { mount }]) => [id, mount]) @@ -154,7 +166,7 @@ export class ApplicationService { return ( path ? `/app/${appId}/${path.replace(/^\//, '')}` // Remove preceding slash from path if present : `/app/${appId}`; + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/core/public/application/capabilities/capabilities_service.tsx b/src/core/public/application/capabilities/capabilities_service.tsx index e330a4b0326aeb..24d9765953c443 100644 --- a/src/core/public/application/capabilities/capabilities_service.tsx +++ b/src/core/public/application/capabilities/capabilities_service.tsx @@ -74,7 +74,7 @@ export class CapabilitiesService { const url = http.anonymousPaths.isAnonymous(window.location.pathname) ? '/api/core/capabilities/defaults' : '/api/core/capabilities'; - const capabilities = await http.post(url, { + const capabilities = await http.post(url, { body: payload, }); return deepFreeze(capabilities); diff --git a/src/core/public/application/index.ts b/src/core/public/application/index.ts index ae25b54cf07a84..9c4427c772a5ea 100644 --- a/src/core/public/application/index.ts +++ b/src/core/public/application/index.ts @@ -22,6 +22,8 @@ export { Capabilities } from './capabilities'; export { App, AppBase, + AppMount, + AppMountDeprecated, AppUnmount, AppMountContext, AppMountParameters, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 593858851d3872..81aef5204c7e29 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs'; import { I18nProvider } from '@kbn/i18n/react'; -import { AppMounter, LegacyApp, AppMountParameters } from '../types'; +import { AppMount, LegacyApp, AppMountParameters } from '../types'; import { httpServiceMock } from '../../http/http_service.mock'; import { AppRouter, AppNotFound } from '../ui'; @@ -35,7 +35,7 @@ const createMountHandler = (htmlString: string) => }); describe('AppContainer', () => { - let apps: Map, Parameters>>; + let apps: Map, Parameters>>; let legacyApps: Map; let history: History; let router: ReactWrapper; diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index a031ab60704134..fd009066fc6640 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -75,12 +75,14 @@ export interface AppBase { */ export interface App extends AppBase { /** - * A mount function called when the user navigates to this app's route. - * @param context The mount context for this app. - * @param targetDomElement An HTMLElement to mount the application onto. - * @returns An unmounting function that will be called to unmount the application. + * A mount function called when the user navigates to this app's route. May have signature of {@link AppMount} or + * {@link AppMountDeprecated}. + * + * @remarks + * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. + * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. */ - mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + mount: AppMount | AppMountDeprecated; /** * Hide the UI chrome when the application is mounted. Defaults to `false`. @@ -97,7 +99,39 @@ export interface LegacyApp extends AppBase { } /** - * The context object received when applications are mounted to the DOM. + * A mount function called when the user navigates to this app's route. + * + * @param params {@link AppMountParameters} + * @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}. + * + * @public + */ +export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; + +/** + * A mount function called when the user navigates to this app's route. + * + * @remarks + * When function has two arguments, it will be called with a {@link AppMountContext | context} as the first argument. + * This behavior is **deprecated**, and consumers should instead use {@link CoreSetup.getStartServices}. + * + * @param context The mount context for this app. Deprecated, use {@link CoreSetup.getStartServices}. + * @param params {@link AppMountParameters} + * @returns An unmounting function that will be called to unmount the application. See {@link AppUnmount}. + * + * @deprecated + * @public + */ +export type AppMountDeprecated = ( + context: AppMountContext, + params: AppMountParameters +) => AppUnmount | Promise; + +/** + * The context object received when applications are mounted to the DOM. Deprecated, use + * {@link CoreSetup.getStartServices}. + * + * @deprecated * @public */ export interface AppMountContext { @@ -155,9 +189,9 @@ export interface AppMountParameters { * setup({ application }) { * application.register({ * id: 'my-app', - * async mount(context, params) { + * async mount(params) { * const { renderApp } = await import('./application'); - * return renderApp(context, params); + * return renderApp(params); * }, * }); * } @@ -170,7 +204,10 @@ export interface AppMountParameters { * import ReactDOM from 'react-dom'; * import { BrowserRouter, Route } from 'react-router-dom'; * - * export renderApp = (context, { appBasePath, element }) => { + * import { CoreStart, AppMountParams } from 'src/core/public'; + * import { MyPluginDepsStart } from './plugin'; + * + * export renderApp = ({ appBasePath, element }: AppMountParams) => { * ReactDOM.render( * // pass `appBasePath` to `basename` * @@ -192,9 +229,6 @@ export interface AppMountParameters { */ export type AppUnmount = () => void; -/** @internal */ -export type AppMounter = (params: AppMountParameters) => Promise; - /** @public */ export interface ApplicationSetup { /** @@ -205,14 +239,15 @@ export interface ApplicationSetup { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -234,8 +269,9 @@ export interface InternalApplicationSetup { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function @@ -243,7 +279,7 @@ export interface InternalApplicationSetup { registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -272,15 +308,16 @@ export interface ApplicationStart { /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function */ registerMountContext( contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; } @@ -301,8 +338,9 @@ export interface InternalApplicationStart /** * Register a context provider for application mounting. Will only be available to applications that depend on the - * plugin that registered this context. + * plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}. * + * @deprecated * @param pluginOpaqueId - The opaque ID of the plugin that is registering the context. * @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to. * @param provider - A {@link IContextProvider} function @@ -310,7 +348,7 @@ export interface InternalApplicationStart registerMountContext( pluginOpaqueId: PluginOpaqueId, contextName: T, - provider: IContextProvider + provider: IContextProvider ): void; // Internal APIs diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 876cd3aa3a3d3a..9c2bb30e795032 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -21,12 +21,12 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { Subject } from 'rxjs'; -import { LegacyApp, AppMounter, AppUnmount } from '../types'; +import { LegacyApp, AppMount, AppUnmount } from '../types'; import { HttpStart } from '../../http'; import { AppNotFound } from './app_not_found_screen'; interface Props extends RouteComponentProps<{ appId: string }> { - apps: ReadonlyMap; + apps: ReadonlyMap; legacyApps: ReadonlyMap; basePath: HttpStart['basePath']; currentAppId$: Subject; diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index b574bf16278e2a..67701a33dabf41 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -22,12 +22,12 @@ import React from 'react'; import { Router, Route } from 'react-router-dom'; import { Subject } from 'rxjs'; -import { LegacyApp, AppMounter } from '../types'; +import { LegacyApp, AppMount } from '../types'; import { AppContainer } from './app_container'; import { HttpStart } from '../../http'; interface Props { - apps: ReadonlyMap; + apps: ReadonlyMap; legacyApps: ReadonlyMap; basePath: HttpStart['basePath']; currentAppId$: Subject; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 4818484b00819c..abc4c144356e8e 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -64,7 +64,7 @@ export interface CoreContext { } /** @internal */ -export interface InternalCoreSetup extends Omit { +export interface InternalCoreSetup extends Omit { application: InternalApplicationSetup; injectedMetadata: InjectedMetadataSetup; } @@ -253,11 +253,11 @@ export class CoreSystem { docLinks, http, i18n, + injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), notifications, overlays, savedObjects, uiSettings, - injectedMetadata: pick(injectedMetadata, ['getInjectedVar']), })); const core: InternalCoreStart = { diff --git a/src/core/public/http/fetch.ts b/src/core/public/http/fetch.ts new file mode 100644 index 00000000000000..472b617cacd7f1 --- /dev/null +++ b/src/core/public/http/fetch.ts @@ -0,0 +1,155 @@ +/* + * 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 { merge } from 'lodash'; +import { format } from 'url'; + +import { IBasePath, HttpInterceptor, HttpHandler, HttpFetchOptions, IHttpResponse } from './types'; +import { HttpFetchError } from './http_fetch_error'; +import { HttpInterceptController } from './http_intercept_controller'; +import { HttpResponse } from './response'; +import { interceptRequest, interceptResponse } from './intercept'; +import { HttpInterceptHaltError } from './http_intercept_halt_error'; + +interface Params { + basePath: IBasePath; + kibanaVersion: string; +} + +const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; +const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; + +export class FetchService { + private readonly interceptors = new Set(); + + constructor(private readonly params: Params) {} + + public intercept(interceptor: HttpInterceptor) { + this.interceptors.add(interceptor); + return () => this.interceptors.delete(interceptor); + } + + public removeAllInterceptors() { + this.interceptors.clear(); + } + + public fetch: HttpHandler = async ( + path: string, + options: HttpFetchOptions = {} + ) => { + const initialRequest = this.createRequest(path, options); + const controller = new HttpInterceptController(); + + // We wrap the interception in a separate promise to ensure that when + // a halt is called we do not resolve or reject, halting handling of the promise. + return new Promise>(async (resolve, reject) => { + try { + const interceptedRequest = await interceptRequest( + initialRequest, + this.interceptors, + controller + ); + const initialResponse = this.fetchResponse(interceptedRequest); + const interceptedResponse = await interceptResponse( + initialResponse, + this.interceptors, + controller + ); + + if (options.asResponse) { + resolve(interceptedResponse); + } else { + resolve(interceptedResponse.body); + } + } catch (error) { + if (!(error instanceof HttpInterceptHaltError)) { + reject(error); + } + } + }); + }; + + private createRequest(path: string, options?: HttpFetchOptions): Request { + // Merge and destructure options out that are not applicable to the Fetch API. + const { query, prependBasePath: shouldPrependBasePath, asResponse, ...fetchOptions } = merge( + { + method: 'GET', + credentials: 'same-origin', + prependBasePath: true, + headers: { + 'kbn-version': this.params.kibanaVersion, + 'Content-Type': 'application/json', + }, + }, + options || {} + ); + const url = format({ + pathname: shouldPrependBasePath ? this.params.basePath.prepend(path) : path, + query, + }); + + if ( + options && + options.headers && + 'Content-Type' in options.headers && + options.headers['Content-Type'] === undefined + ) { + delete fetchOptions.headers['Content-Type']; + } + + return new Request(url, fetchOptions); + } + + private async fetchResponse(request: Request) { + let response: Response; + let body = null; + + try { + response = await window.fetch(request); + } catch (err) { + throw new HttpFetchError(err.message, request); + } + + const contentType = response.headers.get('Content-Type') || ''; + + try { + if (NDJSON_CONTENT.test(contentType)) { + body = await response.blob(); + } else if (JSON_CONTENT.test(contentType)) { + body = await response.json(); + } else { + const text = await response.text(); + + try { + body = JSON.parse(text); + } catch (err) { + body = text; + } + } + } catch (err) { + throw new HttpFetchError(err.message, request, response, body); + } + + if (!response.ok) { + throw new HttpFetchError(response.statusText, request, response, body); + } + + return new HttpResponse({ request, response, body }); + } +} diff --git a/src/core/public/http/http_service.test.ts b/src/core/public/http/http_service.test.ts index 13906b91ed8df2..09f3cca419e4d8 100644 --- a/src/core/public/http/http_service.test.ts +++ b/src/core/public/http/http_service.test.ts @@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock/es5/client'; import { readFileSync } from 'fs'; import { join } from 'path'; import { setup, SetupTap } from '../../../test_utils/public/http_test_setup'; -import { HttpResponse } from './types'; +import { IHttpResponse } from './types'; function delay(duration: number) { return new Promise(r => setTimeout(r, duration)); @@ -101,32 +101,32 @@ describe('http requests', () => { it('should return response', async () => { const { http } = setup(); - fetchMock.get('*', { foo: 'bar' }); - const json = await http.fetch('/my/path'); - expect(json).toEqual({ foo: 'bar' }); }); it('should prepend url with basepath by default', async () => { const { http } = setup(); - fetchMock.get('*', {}); await http.fetch('/my/path'); - expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); }); it('should not prepend url with basepath when disabled', async () => { const { http } = setup(); - fetchMock.get('*', {}); await http.fetch('my/path', { prependBasePath: false }); - expect(fetchMock.lastUrl()).toBe('/my/path'); }); + it('should not include undefined query params', async () => { + const { http } = setup(); + fetchMock.get('*', {}); + await http.fetch('/my/path', { query: { a: undefined } }); + expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path'); + }); + it('should make request with defaults', async () => { const { http } = setup(); @@ -145,6 +145,18 @@ describe('http requests', () => { }); }); + it('should expose detailed response object when asResponse = true', async () => { + const { http } = setup(); + + fetchMock.get('*', { foo: 'bar' }); + + const response = await http.fetch('/my/path', { asResponse: true }); + + expect(response.request).toBeInstanceOf(Request); + expect(response.response).toBeInstanceOf(Response); + expect(response.body).toEqual({ foo: 'bar' }); + }); + it('should reject on network error', async () => { const { http } = setup(); @@ -496,7 +508,7 @@ describe('interception', () => { it('should accumulate response information', async () => { const bodies = ['alpha', 'beta', 'gamma']; - const createResponse = jest.fn((httpResponse: HttpResponse) => ({ + const createResponse = jest.fn((httpResponse: IHttpResponse) => ({ body: bodies.shift(), })); diff --git a/src/core/public/http/http_setup.ts b/src/core/public/http/http_setup.ts index 602382e3a5a60e..c63750849f13af 100644 --- a/src/core/public/http/http_setup.ts +++ b/src/core/public/http/http_setup.ts @@ -27,21 +27,16 @@ import { takeUntil, tap, } from 'rxjs/operators'; -import { merge } from 'lodash'; -import { format } from 'url'; import { InjectedMetadataSetup } from '../injected_metadata'; import { FatalErrorsSetup } from '../fatal_errors'; -import { HttpFetchOptions, HttpServiceBase, HttpInterceptor, HttpResponse } from './types'; +import { HttpFetchOptions, HttpServiceBase } from './types'; import { HttpInterceptController } from './http_intercept_controller'; -import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptHaltError } from './http_intercept_halt_error'; import { BasePath } from './base_path_service'; import { AnonymousPaths } from './anonymous_paths'; +import { FetchService } from './fetch'; -const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/; -const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/; - -function checkHalt(controller: HttpInterceptController, error?: Error) { +export function checkHalt(controller: HttpInterceptController, error?: Error) { if (error instanceof HttpInterceptHaltError) { throw error; } else if (controller.halted) { @@ -55,224 +50,15 @@ export const setup = ( ): HttpServiceBase => { const loadingCount$ = new BehaviorSubject(0); const stop$ = new Subject(); - const interceptors = new Set(); const kibanaVersion = injectedMetadata.getKibanaVersion(); const basePath = new BasePath(injectedMetadata.getBasePath()); const anonymousPaths = new AnonymousPaths(basePath); - function intercept(interceptor: HttpInterceptor) { - interceptors.add(interceptor); - - return () => interceptors.delete(interceptor); - } - - function removeAllInterceptors() { - interceptors.clear(); - } - - function createRequest(path: string, options?: HttpFetchOptions) { - const { query, prependBasePath: shouldPrependBasePath, ...fetchOptions } = merge( - { - method: 'GET', - credentials: 'same-origin', - prependBasePath: true, - headers: { - 'kbn-version': kibanaVersion, - 'Content-Type': 'application/json', - }, - }, - options || {} - ); - const url = format({ - pathname: shouldPrependBasePath ? basePath.prepend(path) : path, - query, - }); - - if ( - options && - options.headers && - 'Content-Type' in options.headers && - options.headers['Content-Type'] === undefined - ) { - delete fetchOptions.headers['Content-Type']; - } - - return new Request(url, fetchOptions); - } - - // Request/response interceptors are called in opposite orders. - // Request hooks start from the newest interceptor and end with the oldest. - function interceptRequest( - request: Request, - controller: HttpInterceptController - ): Promise { - let next = request; - - return [...interceptors].reduceRight( - (promise, interceptor) => - promise.then( - async (current: Request) => { - next = current; - checkHalt(controller); - - if (!interceptor.request) { - return current; - } - - return (await interceptor.request(current, controller)) || current; - }, - async error => { - checkHalt(controller, error); - - if (!interceptor.requestError) { - throw error; - } - - const nextRequest = await interceptor.requestError( - { error, request: next }, - controller - ); - - if (!nextRequest) { - throw error; - } - - next = nextRequest; - return next; - } - ), - Promise.resolve(request) - ); - } - - // Response hooks start from the oldest interceptor and end with the newest. - async function interceptResponse( - responsePromise: Promise, - controller: HttpInterceptController - ) { - let current: HttpResponse | undefined; - - const finalHttpResponse = await [...interceptors].reduce( - (promise, interceptor) => - promise.then( - async httpResponse => { - current = httpResponse; - checkHalt(controller); - - if (!interceptor.response) { - return httpResponse; - } - - return { - ...httpResponse, - ...((await interceptor.response(httpResponse, controller)) || {}), - }; - }, - async error => { - const request = error.request || (current && current.request); - - checkHalt(controller, error); - - if (!interceptor.responseError) { - throw error; - } - - try { - const next = await interceptor.responseError( - { - error, - request, - response: error.response || (current && current.response), - body: error.body || (current && current.body), - }, - controller - ); - - checkHalt(controller, error); - - if (!next) { - throw error; - } - - return { ...next, request }; - } catch (err) { - checkHalt(controller, err); - throw err; - } - } - ), - responsePromise - ); - - return finalHttpResponse.body; - } - - async function fetcher(request: Request): Promise { - let response; - let body = null; - - try { - response = await window.fetch(request); - } catch (err) { - throw new HttpFetchError(err.message, request); - } - - const contentType = response.headers.get('Content-Type') || ''; - - try { - if (NDJSON_CONTENT.test(contentType)) { - body = await response.blob(); - } else if (JSON_CONTENT.test(contentType)) { - body = await response.json(); - } else { - const text = await response.text(); - - try { - body = JSON.parse(text); - } catch (err) { - body = text; - } - } - } catch (err) { - throw new HttpFetchError(err.message, request, response, body); - } - - if (!response.ok) { - throw new HttpFetchError(response.statusText, request, response, body); - } - - return { response, body, request }; - } - - async function fetch(path: string, options: HttpFetchOptions = {}) { - const controller = new HttpInterceptController(); - const initialRequest = createRequest(path, options); - - // We wrap the interception in a separate promise to ensure that when - // a halt is called we do not resolve or reject, halting handling of the promise. - return new Promise(async (resolve, reject) => { - function rejectIfNotHalted(err: any) { - if (!(err instanceof HttpInterceptHaltError)) { - reject(err); - } - } - - try { - const request = await interceptRequest(initialRequest, controller); - - try { - resolve(await interceptResponse(fetcher(request), controller)); - } catch (err) { - rejectIfNotHalted(err); - } - } catch (err) { - rejectIfNotHalted(err); - } - }); - } + const fetchService = new FetchService({ basePath, kibanaVersion }); function shorthand(method: string) { - return (path: string, options: HttpFetchOptions = {}) => fetch(path, { ...options, method }); + return (path: string, options: HttpFetchOptions = {}) => + fetchService.fetch(path, { ...options, method }); } function stop() { @@ -321,9 +107,9 @@ export const setup = ( stop, basePath, anonymousPaths, - intercept, - removeAllInterceptors, - fetch, + intercept: fetchService.intercept.bind(fetchService), + removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService), + fetch: fetchService.fetch.bind(fetchService), delete: shorthand('DELETE'), get: shorthand('GET'), head: shorthand('HEAD'), diff --git a/src/core/public/http/intercept.ts b/src/core/public/http/intercept.ts new file mode 100644 index 00000000000000..e2a16565c61c43 --- /dev/null +++ b/src/core/public/http/intercept.ts @@ -0,0 +1,134 @@ +/* + * 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 { HttpInterceptController } from './http_intercept_controller'; +import { HttpInterceptHaltError } from './http_intercept_halt_error'; +import { HttpInterceptor, IHttpResponse } from './types'; +import { HttpResponse } from './response'; + +export async function interceptRequest( + request: Request, + interceptors: ReadonlySet, + controller: HttpInterceptController +): Promise { + let next = request; + + return [...interceptors].reduceRight( + (promise, interceptor) => + promise.then( + async (current: Request) => { + next = current; + checkHalt(controller); + + if (!interceptor.request) { + return current; + } + + return (await interceptor.request(current, controller)) || current; + }, + async error => { + checkHalt(controller, error); + + if (!interceptor.requestError) { + throw error; + } + + const nextRequest = await interceptor.requestError({ error, request: next }, controller); + + if (!nextRequest) { + throw error; + } + + next = nextRequest; + return next; + } + ), + Promise.resolve(request) + ); +} + +export async function interceptResponse( + responsePromise: Promise, + interceptors: ReadonlySet, + controller: HttpInterceptController +): Promise { + let current: IHttpResponse; + + return await [...interceptors].reduce( + (promise, interceptor) => + promise.then( + async httpResponse => { + current = httpResponse; + checkHalt(controller); + + if (!interceptor.response) { + return httpResponse; + } + + const interceptorOverrides = (await interceptor.response(httpResponse, controller)) || {}; + + return new HttpResponse({ + ...httpResponse, + ...interceptorOverrides, + }); + }, + async error => { + const request = error.request || (current && current.request); + + checkHalt(controller, error); + + if (!interceptor.responseError) { + throw error; + } + + try { + const next = await interceptor.responseError( + { + error, + request, + response: error.response || (current && current.response), + body: error.body || (current && current.body), + }, + controller + ); + + checkHalt(controller, error); + + if (!next) { + throw error; + } + + return new HttpResponse({ ...next, request }); + } catch (err) { + checkHalt(controller, err); + throw err; + } + } + ), + responsePromise + ); +} + +function checkHalt(controller: HttpInterceptController, error?: Error) { + if (error instanceof HttpInterceptHaltError) { + throw error; + } else if (controller.halted) { + throw new HttpInterceptHaltError(); + } +} diff --git a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js b/src/core/public/http/response.ts similarity index 63% rename from src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js rename to src/core/public/http/response.ts index 3eaf18be609d40..706e7caaca9768 100644 --- a/src/legacy/server/config/__tests__/fixtures/run_kbn_server_startup.js +++ b/src/core/public/http/response.ts @@ -17,19 +17,24 @@ * under the License. */ -import { createRoot } from '../../../../../test_utils/kbn_server'; +import { IHttpResponse } from './types'; -(async function run() { - const root = createRoot(JSON.parse(process.env.CREATE_SERVER_OPTS)); +export class HttpResponse implements IHttpResponse { + public readonly request: Request; + public readonly response?: Response; + public readonly body?: TResponseBody; - // We just need the server to run through startup so that it will - // log the deprecation messages. Once it has started up we close it - // to allow the process to exit naturally - try { - await root.setup(); - await root.start(); - } finally { - await root.shutdown(); + constructor({ + request, + response, + body, + }: { + request: Request; + response?: Response; + body?: TResponseBody; + }) { + this.request = request; + this.response = response; + this.body = body; } - -}()); +} diff --git a/src/core/public/http/types.ts b/src/core/public/http/types.ts index 870d4af8f9e861..48385a72325db7 100644 --- a/src/core/public/http/types.ts +++ b/src/core/public/http/types.ts @@ -229,31 +229,49 @@ export interface HttpFetchOptions extends HttpRequestInit { * Headers to send with the request. See {@link HttpHeadersInit}. */ headers?: HttpHeadersInit; + + /** + * When `true` the return type of {@link HttpHandler} will be an {@link IHttpResponse} with detailed request and + * response information. When `false`, the return type will just be the parsed response body. Defaults to `false`. + */ + asResponse?: boolean; } /** * A function for making an HTTP requests to Kibana's backend. See {@link HttpFetchOptions} for options and - * {@link HttpBody} for the response. + * {@link IHttpResponse} for the response. * * @param path the path on the Kibana server to send the request to. Should not include the basePath. * @param options {@link HttpFetchOptions} - * @returns a Promise that resolves to a {@link HttpBody} + * @returns a Promise that resolves to a {@link IHttpResponse} * @public */ -export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; - -/** @public */ -export type HttpBody = BodyInit | null | any; +export interface HttpHandler { + (path: string, options: HttpFetchOptions & { asResponse: true }): Promise< + IHttpResponse + >; + (path: string, options?: HttpFetchOptions): Promise; +} /** @public */ -export interface InterceptedHttpResponse { - response?: Response; - body?: HttpBody; +export interface IHttpResponse { + /** Raw request sent to Kibana server. */ + readonly request: Readonly; + /** Raw response received, may be undefined if there was an error. */ + readonly response?: Readonly; + /** Parsed body received, may be undefined if there was an error. */ + readonly body?: TResponseBody; } -/** @public */ -export interface HttpResponse extends InterceptedHttpResponse { - request: Readonly; +/** + * Properties that can be returned by HttpInterceptor.request to override the response. + * @public + */ +export interface IHttpResponseInterceptorOverrides { + /** Raw response received, may be undefined if there was an error. */ + readonly response?: Readonly; + /** Parsed body received, may be undefined if there was an error. */ + readonly body?: TResponseBody; } /** @public */ @@ -272,7 +290,7 @@ export interface IHttpFetchError extends Error { } /** @public */ -export interface HttpErrorResponse extends HttpResponse { +export interface HttpErrorResponse extends IHttpResponse { error: Error | IHttpFetchError; } /** @public */ @@ -310,13 +328,13 @@ export interface HttpInterceptor { /** * Define an interceptor to be executed after a response is received. - * @param httpResponse {@link HttpResponse} + * @param httpResponse {@link IHttpResponse} * @param controller {@link IHttpInterceptController} */ response?( - httpResponse: HttpResponse, + httpResponse: IHttpResponse, controller: IHttpInterceptController - ): Promise | InterceptedHttpResponse | void; + ): Promise | IHttpResponseInterceptorOverrides | void; /** * Define an interceptor to be executed if a response interceptor throws an error or returns a rejected Promise. @@ -326,7 +344,7 @@ export interface HttpInterceptor { responseError?( httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController - ): Promise | InterceptedHttpResponse | void; + ): Promise | IHttpResponseInterceptorOverrides | void; } /** diff --git a/src/core/public/index.ts b/src/core/public/index.ts index cfec03427f3e7d..f83ca2564de8ee 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -79,7 +79,17 @@ import { export { CoreContext, CoreSystem } from './core_system'; export { RecursiveReadonly } from '../utils'; -export { App, AppBase, AppUnmount, AppMountContext, AppMountParameters } from './application'; +export { + ApplicationSetup, + ApplicationStart, + App, + AppBase, + AppMount, + AppMountDeprecated, + AppUnmount, + AppMountContext, + AppMountParameters, +} from './application'; export { SavedObjectsBatchResponse, @@ -101,6 +111,13 @@ export { SavedObjectsClientContract, SavedObjectsClient, SimpleSavedObject, + SavedObjectsImportResponse, + SavedObjectsImportConflictError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportUnknownError, + SavedObjectsImportError, + SavedObjectsImportRetry, } from './saved_objects'; export { @@ -112,14 +129,13 @@ export { HttpErrorResponse, HttpErrorRequest, HttpInterceptor, - HttpResponse, + IHttpResponse, HttpHandler, - HttpBody, IBasePath, IAnonymousPaths, IHttpInterceptController, IHttpFetchError, - InterceptedHttpResponse, + IHttpResponseInterceptorOverrides, } from './http'; export { OverlayStart, OverlayBannersStart, OverlayRef } from './overlays'; @@ -146,10 +162,13 @@ export { MountPoint, UnmountCallback } from './types'; * navigation in the generated docs until there's a fix for * https://github.com/Microsoft/web-build-tools/issues/1237 */ -export interface CoreSetup { +export interface CoreSetup { /** {@link ApplicationSetup} */ application: ApplicationSetup; - /** {@link ContextSetup} */ + /** + * {@link ContextSetup} + * @deprecated + */ context: ContextSetup; /** {@link FatalErrorsSetup} */ fatalErrors: FatalErrorsSetup; @@ -168,6 +187,13 @@ export interface CoreSetup { injectedMetadata: { getInjectedVar: (name: string, defaultValue?: any) => unknown; }; + + /** + * Allows plugins to get access to APIs available in start inside async + * handlers, such as {@link App.mount}. Promise will not resolve until Core + * and plugin dependencies have completed `start`. + */ + getStartServices(): Promise<[CoreStart, TPluginsStart]>; } /** @@ -219,7 +245,7 @@ export interface CoreStart { * @public * @deprecated */ -export interface LegacyCoreSetup extends CoreSetup { +export interface LegacyCoreSetup extends CoreSetup { /** @deprecated */ injectedMetadata: InjectedMetadataSetup; } @@ -240,8 +266,6 @@ export interface LegacyCoreStart extends CoreStart { } export { - ApplicationSetup, - ApplicationStart, Capabilities, ChromeBadge, ChromeBrand, diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 37e07af0a7da58..9dd24f9e4a7a3c 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -169,6 +169,20 @@ describe('#start()', () => { expect(mockUiNewPlatformStart).toHaveBeenCalledWith(expect.any(Object), {}); }); + it('resolves getStartServices with core and plugin APIs', async () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + legacyPlatform.start(defaultStartDeps); + + const { getStartServices } = mockUiNewPlatformSetup.mock.calls[0][0]; + const [coreStart, pluginsStart] = await getStartServices(); + expect(coreStart).toEqual(expect.any(Object)); + expect(pluginsStart).toBe(defaultStartDeps.plugins); + }); + describe('useLegacyTestHarness = false', () => { it('passes the targetDomElement to ui/chrome', () => { const legacyPlatform = new LegacyPlatformService({ diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 22e315f9e1b030..a4fdd86de53112 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -18,6 +18,8 @@ */ import angular from 'angular'; +import { first } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { InternalCoreSetup, InternalCoreStart } from '../core_system'; import { LegacyCoreSetup, LegacyCoreStart, MountPoint } from '../'; @@ -55,6 +57,8 @@ export class LegacyPlatformService { public readonly legacyId = Symbol(); private bootstrapModule?: BootstrapModule; private targetDomElement?: HTMLElement; + private readonly startDependencies$ = new Subject<[LegacyCoreStart, object]>(); + private readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); constructor(private readonly params: LegacyPlatformParams) {} @@ -75,6 +79,7 @@ export class LegacyPlatformService { const legacyCore: LegacyCoreSetup = { ...core, + getStartServices: () => this.startDependencies, application: { register: notSupported(`core.application.register()`), registerMountContext: notSupported(`core.application.registerMountContext()`), @@ -120,6 +125,8 @@ export class LegacyPlatformService { }, }; + this.startDependencies$.next([legacyCore, plugins]); + // Inject parts of the new platform into parts of the legacy platform // so that legacy APIs/modules can mimic their new platform counterparts require('ui/new_platform').__start__(legacyCore, plugins); diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index 644df259b8e242..43c8aa6f1d6b96 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -46,6 +46,9 @@ function createCoreSetupMock({ basePath = '' } = {}) { application: applicationServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), fatalErrors: fatalErrorsServiceMock.createSetupContract(), + getStartServices: jest.fn, object]>, []>(() => + Promise.resolve([createCoreStartMock({ basePath }), {}]) + ), http: httpServiceMock.createSetupContract({ basePath }), notifications: notificationServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), @@ -75,6 +78,7 @@ function createCoreStartMock({ basePath = '' } = {}) { return mock; } + function pluginInitializerContextMock() { const mock: PluginInitializerContext = { opaqueId: Symbol(), diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 85de5c6620cc16..111ee93dd699b3 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -106,6 +106,33 @@ describe('PluginWrapper', () => { expect(mockPlugin.start).toHaveBeenCalledWith(context, deps); }); + test("`start` resolves `startDependencies` Promise after plugin's start", async () => { + expect.assertions(2); + + let startDependenciesResolved = false; + mockPluginLoader.mockResolvedValueOnce(() => ({ + setup: jest.fn(), + start: async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + })); + await plugin.load(addBasePath); + await plugin.setup({} as any, {} as any); + const context = { any: 'thing' } as any; + const deps = { otherDep: 'value' }; + + // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is + // called. + const startDependenciesCheck = plugin.startDependencies.then(res => { + startDependenciesResolved = true; + expect(res).toEqual([context, deps]); + }); + await plugin.start(context, deps); + await startDependenciesCheck; + }); + test('`stop` fails if plugin is not setup up', async () => { expect(() => plugin.stop()).toThrowErrorMatchingInlineSnapshot( `"Plugin \\"plugin-a\\" can't be stopped since it isn't set up."` diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index 05268bbfcdd05d..e880627e352c88 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -17,6 +17,8 @@ * under the License. */ +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; import { loadPluginBundle } from './plugin_loader'; @@ -33,7 +35,7 @@ export interface Plugin< TPluginsSetup extends object = object, TPluginsStart extends object = object > { - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; stop?(): void; } @@ -70,6 +72,9 @@ export class PluginWrapper< private initializer?: PluginInitializer; private instance?: Plugin; + private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>(); + public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); + constructor( public readonly discoveredPlugin: DiscoveredPlugin, public readonly opaqueId: PluginOpaqueId, @@ -100,7 +105,7 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = await this.createPluginInstance(); return await this.instance.setup(setupContext, plugins); @@ -118,7 +123,11 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - return await this.instance.start(startContext, plugins); + const startContract = await this.instance.start(startContext, plugins); + + this.startDependencies$.next([startContext, plugins]); + + return startContract; } /** diff --git a/src/core/public/plugins/plugin_context.ts b/src/core/public/plugins/plugin_context.ts index f77ddd8f2f6967..848f46605d4deb 100644 --- a/src/core/public/plugins/plugin_context.ts +++ b/src/core/public/plugins/plugin_context.ts @@ -107,6 +107,7 @@ export function createPluginSetupContext< injectedMetadata: { getInjectedVar: deps.injectedMetadata.getInjectedVar, }, + getStartServices: () => plugin.startDependencies, }; } diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index 2983d7583cb493..281778f9420dd6 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -98,6 +98,7 @@ describe('PluginsService', () => { mockSetupContext = { ...mockSetupDeps, application: expect.any(Object), + getStartServices: expect.any(Function), }; mockStartDeps = { application: applicationServiceMock.createInternalStartContract(), diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6fbc7324ce3936..83b4e67c1cb158 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -19,7 +19,7 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type // @public export interface App extends AppBase { chromeless?: boolean; - mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + mount: AppMount | AppMountDeprecated; } // @public (undocumented) @@ -37,7 +37,8 @@ export interface AppBase { // @public (undocumented) export interface ApplicationSetup { register(app: App): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public (undocumented) @@ -50,10 +51,14 @@ export interface ApplicationStart { path?: string; state?: any; }): void; - registerMountContext(contextName: T, provider: IContextProvider): void; + // @deprecated + registerMountContext(contextName: T, provider: IContextProvider): void; } // @public +export type AppMount = (params: AppMountParameters) => AppUnmount | Promise; + +// @public @deprecated export interface AppMountContext { core: { application: Pick; @@ -71,6 +76,9 @@ export interface AppMountContext { }; } +// @public @deprecated +export type AppMountDeprecated = (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise; + // @public (undocumented) export interface AppMountParameters { appBasePath: string; @@ -275,13 +283,14 @@ export interface CoreContext { } // @public -export interface CoreSetup { +export interface CoreSetup { // (undocumented) application: ApplicationSetup; - // (undocumented) + // @deprecated (undocumented) context: ContextSetup; // (undocumented) fatalErrors: FatalErrorsSetup; + getStartServices(): Promise<[CoreStart, TPluginsStart]>; // (undocumented) http: HttpSetup; // @deprecated @@ -464,9 +473,6 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; -// @public (undocumented) -export type HttpBody = BodyInit | null | any; - // @public (undocumented) export interface HttpErrorRequest { // (undocumented) @@ -476,13 +482,14 @@ export interface HttpErrorRequest { } // @public (undocumented) -export interface HttpErrorResponse extends HttpResponse { +export interface HttpErrorResponse extends IHttpResponse { // (undocumented) error: Error | IHttpFetchError; } // @public export interface HttpFetchOptions extends HttpRequestInit { + asResponse?: boolean; headers?: HttpHeadersInit; prependBasePath?: boolean; query?: HttpFetchQuery; @@ -495,7 +502,14 @@ export interface HttpFetchQuery { } // @public -export type HttpHandler = (path: string, options?: HttpFetchOptions) => Promise; +export interface HttpHandler { + // (undocumented) + (path: string, options: HttpFetchOptions & { + asResponse: true; + }): Promise>; + // (undocumented) + (path: string, options?: HttpFetchOptions): Promise; +} // @public (undocumented) export interface HttpHeadersInit { @@ -507,8 +521,8 @@ export interface HttpHeadersInit { export interface HttpInterceptor { request?(request: Request, controller: IHttpInterceptController): Promise | Request | void; requestError?(httpErrorRequest: HttpErrorRequest, controller: IHttpInterceptController): Promise | Request | void; - response?(httpResponse: HttpResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; - responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | InterceptedHttpResponse | void; + response?(httpResponse: IHttpResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; + responseError?(httpErrorResponse: HttpErrorResponse, controller: IHttpInterceptController): Promise | IHttpResponseInterceptorOverrides | void; } // @public @@ -529,12 +543,6 @@ export interface HttpRequestInit { window?: null; } -// @public (undocumented) -export interface HttpResponse extends InterceptedHttpResponse { - // (undocumented) - request: Readonly; -} - // @public (undocumented) export interface HttpServiceBase { addLoadingCount(countSource$: Observable): void; @@ -613,11 +621,16 @@ export interface IHttpInterceptController { } // @public (undocumented) -export interface InterceptedHttpResponse { - // (undocumented) - body?: HttpBody; - // (undocumented) - response?: Response; +export interface IHttpResponse { + readonly body?: TResponseBody; + readonly request: Readonly; + readonly response?: Readonly; +} + +// @public +export interface IHttpResponseInterceptorOverrides { + readonly body?: TResponseBody; + readonly response?: Readonly; } // @public @@ -649,7 +662,7 @@ export interface IUiSettingsClient { } // @public @deprecated -export interface LegacyCoreSetup extends CoreSetup { +export interface LegacyCoreSetup extends CoreSetup { // Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts // // @deprecated (undocumented) @@ -745,7 +758,7 @@ export interface PackageInfo { // @public export interface Plugin { // (undocumented) - setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; + setup(core: CoreSetup, plugins: TPluginsSetup): TSetup | Promise; // (undocumented) start(core: CoreStart, plugins: TPluginsStart): TStart | Promise; // (undocumented) @@ -873,7 +886,7 @@ export class SavedObjectsClient { bulkUpdate(objects?: SavedObjectsBulkUpdateObject[]): Promise>; create: (type: string, attributes: T, options?: SavedObjectsCreateOptions) => Promise>; delete: (type: string, id: string) => Promise<{}>; - find: (options: Pick) => Promise>; + find: (options: Pick) => Promise>; get: (type: string, id: string) => Promise>; update(type: string, id: string, attributes: T, { version, migrationVersion, references }?: SavedObjectsUpdateOptions): Promise>; } @@ -926,6 +939,82 @@ export interface SavedObjectsFindResponsePublic; + // (undocumented) + references: Array<{ + type: string; + id: string; + }>; + // (undocumented) + type: 'missing_references'; +} + +// @public +export interface SavedObjectsImportResponse { + // (undocumented) + errors?: SavedObjectsImportError[]; + // (undocumented) + success: boolean; + // (undocumented) + successCount: number; +} + +// @public +export interface SavedObjectsImportRetry { + // (undocumented) + id: string; + // (undocumented) + overwrite: boolean; + // (undocumented) + replaceReferences: Array<{ + type: string; + from: string; + to: string; + }>; + // (undocumented) + type: string; +} + +// @public +export interface SavedObjectsImportUnknownError { + // (undocumented) + message: string; + // (undocumented) + statusCode: number; + // (undocumented) + type: 'unknown'; +} + +// @public +export interface SavedObjectsImportUnsupportedTypeError { + // (undocumented) + type: 'unsupported_type'; +} + // @public export interface SavedObjectsMigrationVersion { // (undocumented) diff --git a/src/core/public/saved_objects/index.ts b/src/core/public/saved_objects/index.ts index 453c3e42a1687b..5015a9c3db78e2 100644 --- a/src/core/public/saved_objects/index.ts +++ b/src/core/public/saved_objects/index.ts @@ -40,4 +40,11 @@ export { SavedObjectsBaseOptions, SavedObjectsFindOptions, SavedObjectsMigrationVersion, + SavedObjectsImportResponse, + SavedObjectsImportConflictError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportUnknownError, + SavedObjectsImportError, + SavedObjectsImportRetry, } from '../../server/types'; diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index a0cf3d1602879e..dff0c00a4625e7 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -20,7 +20,6 @@ import chalk from 'chalk'; import { isMaster } from 'cluster'; import { CliArgs, Env, RawConfigService } from './config'; -import { LegacyObjectToConfigAdapter } from './legacy'; import { Root } from './root'; import { CriticalError } from './errors'; @@ -62,14 +61,10 @@ export async function bootstrap({ isDevClusterMaster: isMaster && cliArgs.dev && features.isClusterModeSupported, }); - const rawConfigService = new RawConfigService( - env.configs, - rawConfig => new LegacyObjectToConfigAdapter(applyConfigOverrides(rawConfig)) - ); - + const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides); rawConfigService.loadConfig(); - const root = new Root(rawConfigService.getConfig$(), env, onRootShutdown); + const root = new Root(rawConfigService, env, onRootShutdown); process.on('SIGHUP', () => reloadLoggingConfig()); diff --git a/src/core/server/config/config_service.mock.ts b/src/core/server/config/config_service.mock.ts index e87869e92deebc..b05b13d9e2d510 100644 --- a/src/core/server/config/config_service.mock.ts +++ b/src/core/server/config/config_service.mock.ts @@ -34,6 +34,8 @@ const createConfigServiceMock = ({ getUnusedPaths: jest.fn(), isEnabledAtPath: jest.fn(), setSchema: jest.fn(), + addDeprecationProvider: jest.fn(), + validate: jest.fn(), }; mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); diff --git a/src/core/server/config/config_service.test.mocks.ts b/src/core/server/config/config_service.test.mocks.ts index 8fa1ec997d6258..1299c4c0b4eb1a 100644 --- a/src/core/server/config/config_service.test.mocks.ts +++ b/src/core/server/config/config_service.test.mocks.ts @@ -19,3 +19,8 @@ export const mockPackage = new Proxy({ raw: {} as any }, { get: (obj, prop) => obj.raw[prop] }); jest.mock('../../../../package.json', () => mockPackage); + +export const mockApplyDeprecations = jest.fn((config, deprecations, log) => config); +jest.mock('./deprecation/apply_deprecations', () => ({ + applyDeprecations: mockApplyDeprecations, +})); diff --git a/src/core/server/config/config_service.test.ts b/src/core/server/config/config_service.test.ts index 131e1dd5017928..773a444dea948a 100644 --- a/src/core/server/config/config_service.test.ts +++ b/src/core/server/config/config_service.test.ts @@ -20,13 +20,14 @@ /* eslint-disable max-classes-per-file */ import { BehaviorSubject, Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, take } from 'rxjs/operators'; -import { mockPackage } from './config_service.test.mocks'; +import { mockPackage, mockApplyDeprecations } from './config_service.test.mocks'; +import { rawConfigServiceMock } from './raw_config_service.mock'; import { schema } from '@kbn/config-schema'; -import { ConfigService, Env, ObjectToConfigAdapter } from '.'; +import { ConfigService, Env } from '.'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { getEnvOptions } from './__mocks__/env'; @@ -34,9 +35,12 @@ const emptyArgv = getEnvOptions(); const defaultEnv = new Env('/kibana', emptyArgv); const logger = loggingServiceMock.create(); +const getRawConfigProvider = (rawConfig: Record) => + rawConfigServiceMock.create({ rawConfig }); + test('returns config at path as observable', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'foo' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({ key: 'foo' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); const stringSchema = schema.string(); await configService.setSchema('key', stringSchema); @@ -48,21 +52,36 @@ test('returns config at path as observable', async () => { }); test('throws if config at path does not match schema', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 123 })); + const rawConfig = getRawConfigProvider({ key: 123 }); - const configService = new ConfigService(config$, defaultEnv, logger); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('key', schema.string()); - await expect( - configService.setSchema('key', schema.string()) - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"[config validation of [key]]: expected value of type [string] but got [number]"` - ); + const valuesReceived: any[] = []; + await configService + .atPath('key') + .pipe(take(1)) + .subscribe( + value => { + valuesReceived.push(value); + }, + error => { + valuesReceived.push(error); + } + ); + + await expect(valuesReceived).toMatchInlineSnapshot(` + Array [ + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] + `); }); test('re-validate config when updated', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); - const configService = new ConfigService(config$, defaultEnv, logger); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -75,19 +94,19 @@ test('re-validate config when updated', async () => { } ); - config$.next(new ObjectToConfigAdapter({ key: 123 })); + rawConfig$.next({ key: 123 }); await expect(valuesReceived).toMatchInlineSnapshot(` - Array [ - "value", - [Error: [config validation of [key]]: expected value of type [string] but got [number]], - ] + Array [ + "value", + [Error: [config validation of [key]]: expected value of type [string] but got [number]], + ] `); }); test("returns undefined if fetching optional config at a path that doesn't exist", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); const value$ = configService.optionalAtPath('unique-name'); const value = await value$.pipe(first()).toPromise(); @@ -96,8 +115,8 @@ test("returns undefined if fetching optional config at a path that doesn't exist }); test('returns observable config at optional path if it exists', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ value: 'bar' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig = getRawConfigProvider({ value: 'bar' }); + const configService = new ConfigService(rawConfig, defaultEnv, logger); await configService.setSchema('value', schema.string()); const value$ = configService.optionalAtPath('value'); @@ -107,8 +126,10 @@ test('returns observable config at optional path if it exists', async () => { }); test("does not push new configs when reloading if config at path hasn't changed", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -116,14 +137,16 @@ test("does not push new configs when reloading if config at path hasn't changed" valuesReceived.push(value); }); - config$.next(new ObjectToConfigAdapter({ key: 'value' })); + rawConfig$.next({ key: 'value' }); expect(valuesReceived).toEqual(['value']); }); test('pushes new config when reloading and config at path has changed', async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema('key', schema.string()); const valuesReceived: any[] = []; @@ -131,14 +154,14 @@ test('pushes new config when reloading and config at path has changed', async () valuesReceived.push(value); }); - config$.next(new ObjectToConfigAdapter({ key: 'new value' })); + rawConfig$.next({ key: 'new value' }); expect(valuesReceived).toEqual(['value', 'new value']); }); test("throws error if 'schema' is not defined for a key", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const configs = configService.atPath('key'); @@ -148,8 +171,8 @@ test("throws error if 'schema' is not defined for a key", async () => { }); test("throws error if 'setSchema' called several times for the same key", async () => { - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ key: 'value' })); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { key: 'value' } }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const addSchema = async () => await configService.setSchema('key', schema.string()); await addSchema(); await expect(addSchema()).rejects.toMatchInlineSnapshot( @@ -157,6 +180,32 @@ test("throws error if 'setSchema' called several times for the same key", async ); }); +test('flags schema paths as handled when registering a schema', async () => { + const rawConfigProvider = rawConfigServiceMock.create({ + rawConfig: { + service: { + string: 'str', + number: 42, + }, + }, + }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await configService.setSchema( + 'service', + schema.object({ + string: schema.string(), + number: schema.number(), + }) + ); + + expect(await configService.getUsedPaths()).toMatchInlineSnapshot(` + Array [ + "service.string", + "service.number", + ] + `); +}); + test('tracks unhandled paths', async () => { const initialConfig = { bar: { @@ -178,8 +227,8 @@ test('tracks unhandled paths', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); configService.atPath('foo'); configService.atPath(['bar', 'deep2']); @@ -201,8 +250,8 @@ test('correctly passes context', async () => { }; const env = new Env('/kibana', getEnvOptions()); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: { foo: {} } }); - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({ foo: {} })); const schemaDefinition = schema.object({ branchRef: schema.string({ defaultValue: schema.contextRef('branch'), @@ -219,7 +268,7 @@ test('correctly passes context', async () => { defaultValue: schema.contextRef('version'), }), }); - const configService = new ConfigService(config$, env, logger); + const configService = new ConfigService(rawConfigProvider, env, logger); await configService.setSchema('foo', schemaDefinition); const value$ = configService.atPath('foo'); @@ -234,8 +283,8 @@ test('handles enabled path, but only marks the enabled path as used', async () = }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(true); @@ -252,8 +301,8 @@ test('handles enabled path when path is array', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath(['pid']); expect(isEnabled).toBe(true); @@ -270,8 +319,8 @@ test('handles disabled path and marks config as used', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(false); @@ -287,9 +336,9 @@ test('does not throw if schema does not define "enabled" schema', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); - expect( + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await expect( configService.setSchema( 'pid', schema.object({ @@ -310,8 +359,8 @@ test('does not throw if schema does not define "enabled" schema', async () => { test('treats config as enabled if config path is not present in config', async () => { const initialConfig = {}; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('pid'); expect(isEnabled).toBe(true); @@ -327,8 +376,8 @@ test('read "enabled" even if its schema is not present', async () => { }, }; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); const isEnabled = await configService.isEnabledAtPath('foo'); expect(isEnabled).toBe(true); @@ -337,8 +386,8 @@ test('read "enabled" even if its schema is not present', async () => { test('allows plugins to specify "enabled" flag via validation schema', async () => { const initialConfig = {}; - const config$ = new BehaviorSubject(new ObjectToConfigAdapter(initialConfig)); - const configService = new ConfigService(config$, defaultEnv, logger); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig: initialConfig }); + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); await configService.setSchema( 'foo', @@ -361,3 +410,49 @@ test('allows plugins to specify "enabled" flag via validation schema', async () expect(await configService.isEnabledAtPath('baz')).toBe(true); }); + +test('does not throw during validation is every schema is valid', async () => { + const rawConfig = getRawConfigProvider({ stringKey: 'foo', numberKey: 42 }); + + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('stringKey', schema.string()); + await configService.setSchema('numberKey', schema.number()); + + await expect(configService.validate()).resolves.toBeUndefined(); +}); + +test('throws during validation is any schema is invalid', async () => { + const rawConfig = getRawConfigProvider({ stringKey: 123, numberKey: 42 }); + + const configService = new ConfigService(rawConfig, defaultEnv, logger); + await configService.setSchema('stringKey', schema.string()); + await configService.setSchema('numberKey', schema.number()); + + await expect(configService.validate()).rejects.toThrowErrorMatchingInlineSnapshot( + `"[config validation of [stringKey]]: expected value of type [string] but got [number]"` + ); +}); + +test('logs deprecation warning during validation', async () => { + const rawConfig = getRawConfigProvider({}); + const configService = new ConfigService(rawConfig, defaultEnv, logger); + + mockApplyDeprecations.mockImplementationOnce((config, deprecations, log) => { + log('some deprecation message'); + log('another deprecation message'); + return config; + }); + + loggingServiceMock.clear(logger); + await configService.validate(); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "some deprecation message", + ], + Array [ + "another deprecation message", + ], + ] + `); +}); diff --git a/src/core/server/config/config_service.ts b/src/core/server/config/config_service.ts index c18a5b2000e011..61630f43bffb55 100644 --- a/src/core/server/config/config_service.ts +++ b/src/core/server/config/config_service.ts @@ -19,12 +19,20 @@ import { Type } from '@kbn/config-schema'; import { isEqual } from 'lodash'; -import { Observable } from 'rxjs'; -import { distinctUntilChanged, first, map } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { distinctUntilChanged, first, map, shareReplay, take } from 'rxjs/operators'; import { Config, ConfigPath, Env } from '.'; import { Logger, LoggerFactory } from '../logging'; import { hasConfigPathIntersection } from './config'; +import { RawConfigurationProvider } from './raw_config_service'; +import { + applyDeprecations, + ConfigDeprecationWithContext, + ConfigDeprecationProvider, + configDeprecationFactory, +} from './deprecation'; +import { LegacyObjectToConfigAdapter } from '../legacy/config'; /** @internal */ export type IConfigService = PublicMethodsOf; @@ -32,6 +40,9 @@ export type IConfigService = PublicMethodsOf; /** @internal */ export class ConfigService { private readonly log: Logger; + private readonly deprecationLog: Logger; + + private readonly config$: Observable; /** * Whenever a config if read at a path, we mark that path as 'handled'. We can @@ -39,13 +50,23 @@ export class ConfigService { */ private readonly handledPaths: ConfigPath[] = []; private readonly schemas = new Map>(); + private readonly deprecations = new BehaviorSubject([]); constructor( - private readonly config$: Observable, + private readonly rawConfigProvider: RawConfigurationProvider, private readonly env: Env, logger: LoggerFactory ) { this.log = logger.get('config'); + this.deprecationLog = logger.get('config', 'deprecation'); + + this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe( + map(([rawConfig, deprecations]) => { + const migrated = applyDeprecations(rawConfig, deprecations); + return new LegacyObjectToConfigAdapter(migrated); + }), + shareReplay(1) + ); } /** @@ -58,10 +79,37 @@ export class ConfigService { } this.schemas.set(namespace, schema); + this.markAsHandled(path); + } - await this.validateConfig(path) - .pipe(first()) - .toPromise(); + /** + * Register a {@link ConfigDeprecationProvider} to be used when validating and migrating the configuration + */ + public addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider) { + const flatPath = pathToString(path); + this.deprecations.next([ + ...this.deprecations.value, + ...provider(configDeprecationFactory).map(deprecation => ({ + deprecation, + path: flatPath, + })), + ]); + } + + /** + * Validate the whole configuration and log the deprecation warnings. + * + * This must be done after every schemas and deprecation providers have been registered. + */ + public async validate() { + const namespaces = [...this.schemas.keys()]; + for (let i = 0; i < namespaces.length; i++) { + await this.validateConfigAtPath(namespaces[i]) + .pipe(first()) + .toPromise(); + } + + await this.logDeprecation(); } /** @@ -79,7 +127,7 @@ export class ConfigService { * @param path - The path to the desired subset of the config. */ public atPath(path: ConfigPath) { - return this.validateConfig(path) as Observable; + return this.validateConfigAtPath(path) as Observable; } /** @@ -92,7 +140,7 @@ export class ConfigService { return this.getDistinctConfig(path).pipe( map(config => { if (config === undefined) return undefined; - return this.validate(path, config) as TSchema; + return this.validateAtPath(path, config) as TSchema; }) ); } @@ -148,7 +196,21 @@ export class ConfigService { return config.getFlattenedPaths().filter(path => isPathHandled(path, handledPaths)); } - private validate(path: ConfigPath, config: Record) { + private async logDeprecation() { + const rawConfig = await this.rawConfigProvider + .getConfig$() + .pipe(take(1)) + .toPromise(); + const deprecations = await this.deprecations.pipe(take(1)).toPromise(); + const deprecationMessages: string[] = []; + const logger = (msg: string) => deprecationMessages.push(msg); + applyDeprecations(rawConfig, deprecations, logger); + deprecationMessages.forEach(msg => { + this.deprecationLog.warn(msg); + }); + } + + private validateAtPath(path: ConfigPath, config: Record) { const namespace = pathToString(path); const schema = this.schemas.get(namespace); if (!schema) { @@ -165,8 +227,8 @@ export class ConfigService { ); } - private validateConfig(path: ConfigPath) { - return this.getDistinctConfig(path).pipe(map(config => this.validate(path, config))); + private validateConfigAtPath(path: ConfigPath) { + return this.getDistinctConfig(path).pipe(map(config => this.validateAtPath(path, config))); } private getDistinctConfig(path: ConfigPath) { diff --git a/src/core/server/config/deprecation/apply_deprecations.test.ts b/src/core/server/config/deprecation/apply_deprecations.test.ts new file mode 100644 index 00000000000000..25cae80d8b5cbe --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.test.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { applyDeprecations } from './apply_deprecations'; +import { ConfigDeprecation, ConfigDeprecationWithContext } from './types'; +import { configDeprecationFactory as deprecations } from './deprecation_factory'; + +const wrapHandler = ( + handler: ConfigDeprecation, + path: string = '' +): ConfigDeprecationWithContext => ({ + deprecation: handler, + path, +}); + +describe('applyDeprecations', () => { + it('calls all deprecations handlers once', () => { + const handlerA = jest.fn(); + const handlerB = jest.fn(); + const handlerC = jest.fn(); + applyDeprecations( + {}, + [handlerA, handlerB, handlerC].map(h => wrapHandler(h)) + ); + expect(handlerA).toHaveBeenCalledTimes(1); + expect(handlerB).toHaveBeenCalledTimes(1); + expect(handlerC).toHaveBeenCalledTimes(1); + }); + + it('calls handlers with correct arguments', () => { + const logger = () => undefined; + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + const alteredConfig = { foo: 'bar' }; + + const handlerA = jest.fn().mockReturnValue(alteredConfig); + const handlerB = jest.fn().mockImplementation(conf => conf); + + applyDeprecations( + initialConfig, + [wrapHandler(handlerA, 'pathA'), wrapHandler(handlerB, 'pathB')], + logger + ); + + expect(handlerA).toHaveBeenCalledWith(initialConfig, 'pathA', logger); + expect(handlerB).toHaveBeenCalledWith(alteredConfig, 'pathB', logger); + }); + + it('returns the migrated config', () => { + const initialConfig = { foo: 'bar', deprecated: 'deprecated', renamed: 'renamed' }; + + const migrated = applyDeprecations(initialConfig, [ + wrapHandler(deprecations.unused('deprecated')), + wrapHandler(deprecations.rename('renamed', 'newname')), + ]); + + expect(migrated).toEqual({ foo: 'bar', newname: 'renamed' }); + }); + + it('does not alter the initial config', () => { + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + + const migrated = applyDeprecations(initialConfig, [ + wrapHandler(deprecations.unused('deprecated')), + ]); + + expect(initialConfig).toEqual({ foo: 'bar', deprecated: 'deprecated' }); + expect(migrated).toEqual({ foo: 'bar' }); + }); +}); diff --git a/src/core/server/config/deprecation/apply_deprecations.ts b/src/core/server/config/deprecation/apply_deprecations.ts new file mode 100644 index 00000000000000..f7f95709ed846b --- /dev/null +++ b/src/core/server/config/deprecation/apply_deprecations.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep } from 'lodash'; +import { ConfigDeprecationWithContext, ConfigDeprecationLogger } from './types'; + +const noopLogger = (msg: string) => undefined; + +/** + * Applies deprecations on given configuration and logs any deprecation warning using provided logger. + * + * @internal + */ +export const applyDeprecations = ( + config: Record, + deprecations: ConfigDeprecationWithContext[], + logger: ConfigDeprecationLogger = noopLogger +) => { + let processed = cloneDeep(config); + deprecations.forEach(({ deprecation, path }) => { + processed = deprecation(processed, path, logger); + }); + return processed; +}; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts new file mode 100644 index 00000000000000..b40dbdc1b66519 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -0,0 +1,211 @@ +/* + * 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 { coreDeprecationProvider } from './core_deprecations'; +import { configDeprecationFactory } from './deprecation_factory'; +import { applyDeprecations } from './apply_deprecations'; + +const initialEnv = { ...process.env }; + +const applyCoreDeprecations = (settings: Record = {}) => { + const deprecations = coreDeprecationProvider(configDeprecationFactory); + const deprecationMessages: string[] = []; + const migrated = applyDeprecations( + settings, + deprecations.map(deprecation => ({ + deprecation, + path: '', + })), + msg => deprecationMessages.push(msg) + ); + return { + messages: deprecationMessages, + migrated, + }; +}; + +describe('core deprecations', () => { + beforeEach(() => { + process.env = { ...initialEnv }; + }); + + describe('configPath', () => { + it('logs a warning if CONFIG_PATH environ variable is set', () => { + process.env.CONFIG_PATH = 'somepath'; + const { messages } = applyCoreDeprecations(); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder", + ] + `); + }); + + it('does not log a warning if CONFIG_PATH environ variable is unset', () => { + delete process.env.CONFIG_PATH; + const { messages } = applyCoreDeprecations(); + expect(messages).toHaveLength(0); + }); + }); + + describe('dataPath', () => { + it('logs a warning if DATA_PATH environ variable is set', () => { + process.env.DATA_PATH = 'somepath'; + const { messages } = applyCoreDeprecations(); + expect(messages).toMatchInlineSnapshot(` + Array [ + "Environment variable \\"DATA_PATH\\" will be removed. It has been replaced with kibana.yml setting \\"path.data\\"", + ] + `); + }); + + it('does not log a warning if DATA_PATH environ variable is unset', () => { + delete process.env.DATA_PATH; + const { messages } = applyCoreDeprecations(); + expect(messages).toHaveLength(0); + }); + }); + + describe('rewriteBasePath', () => { + it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { + const { messages } = applyCoreDeprecations({ + server: { + basePath: 'foo', + }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana will expect that all requests start with server.basePath rather than expecting you to rewrite the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the current behavior and silence this warning.", + ] + `); + }); + + it('does not log a warning if both server.basePath and server.rewriteBasePath are unset', () => { + const { messages } = applyCoreDeprecations({ + server: {}, + }); + expect(messages).toHaveLength(0); + }); + + it('does not log a warning if both server.basePath and server.rewriteBasePath are set', () => { + const { messages } = applyCoreDeprecations({ + server: { + basePath: 'foo', + rewriteBasePath: true, + }, + }); + expect(messages).toHaveLength(0); + }); + }); + + describe('cspRulesDeprecation', () => { + describe('with nonce source', () => { + it('logs a warning', () => { + const settings = { + csp: { + rules: [`script-src 'self' 'nonce-{nonce}'`], + }, + }; + const { messages } = applyCoreDeprecations(settings); + expect(messages).toMatchInlineSnapshot(` + Array [ + "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", + ] + `); + }); + + it('replaces a nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }).migrated.csp + .rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }) + .migrated.csp.rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + + it('removes a quoted nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a non-quoted nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' nonce-{nonce}`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + expect( + applyCoreDeprecations({ csp: { rules: [`script-src nonce-{nonce} 'self'`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes a strange nonce', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }).migrated + .csp.rules + ).toEqual([`script-src 'self'`]); + }); + + it('removes multiple nonces', () => { + expect( + applyCoreDeprecations({ + csp: { + rules: [ + `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, + `style-src 'nonce-{nonce}' 'self'`, + ], + }, + }).migrated.csp.rules + ).toEqual([`script-src 'self'`, `style-src 'self'`]); + }); + }); + + describe('without self source', () => { + it('logs a warning', () => { + const { messages } = applyCoreDeprecations({ + csp: { rules: [`script-src 'unsafe-eval'`] }, + }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "csp.rules must contain the 'self' source. Automatically adding to script-src.", + ] + `); + }); + + it('adds self', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }).migrated.csp.rules + ).toEqual([`script-src 'unsafe-eval' 'self'`]); + }); + }); + + it('does not add self to other policies', () => { + expect( + applyCoreDeprecations({ csp: { rules: [`worker-src blob:`] } }).migrated.csp.rules + ).toEqual([`worker-src blob:`]); + }); + }); +}); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts new file mode 100644 index 00000000000000..6a401ec6625a20 --- /dev/null +++ b/src/core/server/config/deprecation/core_deprecations.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 { has, get } from 'lodash'; +import { ConfigDeprecationProvider, ConfigDeprecation } from './types'; + +const configPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(process.env, 'CONFIG_PATH')) { + log( + `Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder` + ); + } + return settings; +}; + +const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(process.env, 'DATA_PATH')) { + log( + `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"` + ); + } + return settings; +}; + +const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + if (has(settings, 'server.basePath') && !has(settings, 'server.rewriteBasePath')) { + log( + 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + + 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + + 'current behavior and silence this warning.' + ); + } + return settings; +}; + +const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => { + const NONCE_STRING = `{nonce}`; + // Policies that should include the 'self' source + const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); + const SELF_STRING = `'self'`; + + const rules: string[] = get(settings, 'csp.rules'); + if (rules) { + const parsed = new Map( + rules.map(ruleStr => { + const parts = ruleStr.split(/\s+/); + return [parts[0], parts.slice(1)]; + }) + ); + + settings.csp.rules = [...parsed].map(([policy, sourceList]) => { + if (sourceList.find(source => source.includes(NONCE_STRING))) { + log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); + sourceList = sourceList.filter(source => !source.includes(NONCE_STRING)); + + // Add 'self' if not present + if (!sourceList.find(source => source.includes(SELF_STRING))) { + sourceList.push(SELF_STRING); + } + } + + if ( + SELF_POLICIES.includes(policy) && + !sourceList.find(source => source.includes(SELF_STRING)) + ) { + log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); + sourceList.push(SELF_STRING); + } + + return `${policy} ${sourceList.join(' ')}`.trim(); + }); + } + + return settings; +}; + +export const coreDeprecationProvider: ConfigDeprecationProvider = ({ + unusedFromRoot, + renameFromRoot, +}) => [ + unusedFromRoot('savedObjects.indexCheckTimeout'), + unusedFromRoot('server.xsrf.token'), + unusedFromRoot('uiSettings.enabled'), + renameFromRoot('optimize.lazy', 'optimize.watch'), + renameFromRoot('optimize.lazyPort', 'optimize.watchPort'), + renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), + renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), + renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), + renameFromRoot('xpack.telemetry.enabled', 'telemetry.enabled'), + renameFromRoot('xpack.telemetry.config', 'telemetry.config'), + renameFromRoot('xpack.telemetry.banner', 'telemetry.banner'), + renameFromRoot('xpack.telemetry.url', 'telemetry.url'), + configPathDeprecation, + dataPathDeprecation, + rewriteBasePathDeprecation, + cspRulesDeprecation, +]; diff --git a/src/core/server/config/deprecation/deprecation_factory.test.ts b/src/core/server/config/deprecation/deprecation_factory.test.ts new file mode 100644 index 00000000000000..2595fdd923dd58 --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.test.ts @@ -0,0 +1,379 @@ +/* + * 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 { ConfigDeprecationLogger } from './types'; +import { configDeprecationFactory } from './deprecation_factory'; + +describe('DeprecationFactory', () => { + const { rename, unused, renameFromRoot, unusedFromRoot } = configDeprecationFactory; + + let deprecationMessages: string[]; + const logger: ConfigDeprecationLogger = msg => deprecationMessages.push(msg); + + beforeEach(() => { + deprecationMessages = []; + }); + + describe('rename', () => { + it('moves the property to rename and logs a warning if old property exist and new one does not', () => { + const rawConfig = { + myplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + renamed: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + ] + `); + }); + it('does not alter config and does not log if old property is not present', () => { + const rawConfig = { + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('deprecated', 'new')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + it('handles nested keys', () => { + const rawConfig = { + myplugin: { + oldsection: { + deprecated: 'toberenamed', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = rename('oldsection.deprecated', 'newsection.renamed')( + rawConfig, + 'myplugin', + logger + ); + expect(processed).toEqual({ + myplugin: { + oldsection: {}, + newsection: { + renamed: 'toberenamed', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + ] + `); + }); + it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + renamed: 'renamed', + }, + }; + const processed = rename('deprecated', 'renamed')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + renamed: 'renamed', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + ] + `); + }); + }); + + describe('renameFromRoot', () => { + it('moves the property from root and logs a warning if old property exist and new one does not', () => { + const rawConfig = { + myplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + renamed: 'toberenamed', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + ] + `); + }); + + it('can move a property to a different namespace', () => { + const rawConfig = { + oldplugin: { + deprecated: 'toberenamed', + valid: 'valid', + }, + newplugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('oldplugin.deprecated', 'newplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + oldplugin: { + valid: 'valid', + }, + newplugin: { + renamed: 'toberenamed', + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + ] + `); + }); + + it('does not alter config and does not log if old property is not present', () => { + const rawConfig = { + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.new')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + new: 'new', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + + it('remove the old property but does not overrides the new one if they both exist, and logs a specific message', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + renamed: 'renamed', + }, + }; + const processed = renameFromRoot('myplugin.deprecated', 'myplugin.renamed')( + rawConfig, + 'does-not-matter', + logger + ); + expect(processed).toEqual({ + myplugin: { + renamed: 'renamed', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + ] + `); + }); + }); + + describe('unused', () => { + it('removes the unused property from the config and logs a warning is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('handles deeply nested keys', () => { + const rawConfig = { + myplugin: { + section: { + deprecated: 'deprecated', + }, + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('section.deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + section: {}, + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.section.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('does not alter config and does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unused('deprecated')(rawConfig, 'myplugin', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + }); + + describe('unusedFromRoot', () => { + it('removes the unused property from the root config and logs a warning is present', () => { + const rawConfig = { + myplugin: { + deprecated: 'deprecated', + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages).toMatchInlineSnapshot(` + Array [ + "myplugin.deprecated is deprecated and is no longer used", + ] + `); + }); + + it('does not alter config and does not log if unused property is not present', () => { + const rawConfig = { + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }; + const processed = unusedFromRoot('myplugin.deprecated')(rawConfig, 'does-not-matter', logger); + expect(processed).toEqual({ + myplugin: { + valid: 'valid', + }, + someOtherPlugin: { + property: 'value', + }, + }); + expect(deprecationMessages.length).toEqual(0); + }); + }); +}); diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts new file mode 100644 index 00000000000000..6f7ed4c4e84cc6 --- /dev/null +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -0,0 +1,95 @@ +/* + * 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 { get, set } from 'lodash'; +import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; +import { unset } from '../../../utils'; + +const _rename = ( + config: Record, + rootPath: string, + log: ConfigDeprecationLogger, + oldKey: string, + newKey: string +) => { + const fullOldPath = getPath(rootPath, oldKey); + const oldValue = get(config, fullOldPath); + if (oldValue === undefined) { + return config; + } + + unset(config, fullOldPath); + + const fullNewPath = getPath(rootPath, newKey); + const newValue = get(config, fullNewPath); + if (newValue === undefined) { + set(config, fullNewPath, oldValue); + 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}"` + ); + } + return config; +}; + +const _unused = ( + config: Record, + rootPath: string, + log: ConfigDeprecationLogger, + unusedKey: string +) => { + const fullPath = getPath(rootPath, unusedKey); + if (get(config, fullPath) === undefined) { + return config; + } + unset(config, fullPath); + log(`${fullPath} is deprecated and is no longer used`); + return config; +}; + +const rename = (oldKey: string, newKey: string): ConfigDeprecation => (config, rootPath, log) => + _rename(config, rootPath, log, oldKey, newKey); + +const renameFromRoot = (oldKey: string, newKey: string): ConfigDeprecation => ( + config, + rootPath, + log +) => _rename(config, '', log, oldKey, newKey); + +const unused = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => + _unused(config, rootPath, log, unusedKey); + +const unusedFromRoot = (unusedKey: string): ConfigDeprecation => (config, rootPath, log) => + _unused(config, '', log, unusedKey); + +const getPath = (rootPath: string, subPath: string) => + rootPath !== '' ? `${rootPath}.${subPath}` : subPath; + +/** + * The actual platform implementation of {@link ConfigDeprecationFactory} + * + * @internal + */ +export const configDeprecationFactory: ConfigDeprecationFactory = { + rename, + renameFromRoot, + unused, + unusedFromRoot, +}; diff --git a/src/core/server/config/deprecation/index.ts b/src/core/server/config/deprecation/index.ts new file mode 100644 index 00000000000000..f79338665166be --- /dev/null +++ b/src/core/server/config/deprecation/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + ConfigDeprecation, + ConfigDeprecationWithContext, + ConfigDeprecationLogger, + ConfigDeprecationFactory, + ConfigDeprecationProvider, +} from './types'; +export { configDeprecationFactory } from './deprecation_factory'; +export { coreDeprecationProvider } from './core_deprecations'; +export { applyDeprecations } from './apply_deprecations'; diff --git a/src/core/server/config/deprecation/types.ts b/src/core/server/config/deprecation/types.ts new file mode 100644 index 00000000000000..19fba7800c919d --- /dev/null +++ b/src/core/server/config/deprecation/types.ts @@ -0,0 +1,141 @@ +/* + * 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. + */ + +/** + * Logger interface used when invoking a {@link ConfigDeprecation} + * + * @public + */ +export type ConfigDeprecationLogger = (message: string) => void; + +/** + * Configuration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration. + * + * @remarks + * This should only be manually implemented if {@link ConfigDeprecationFactory} does not provide the proper helpers for a specific + * deprecation need. + * + * @public + */ +export type ConfigDeprecation = ( + config: Record, + fromPath: string, + logger: ConfigDeprecationLogger +) => Record; + +/** + * A provider that should returns a list of {@link ConfigDeprecation}. + * + * See {@link ConfigDeprecationFactory} for more usage examples. + * + * @example + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * rename('oldKey', 'newKey'), + * unused('deprecatedKey'), + * myCustomDeprecation, + * ] + * ``` + * + * @public + */ +export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; + +/** + * Provides helpers to generates the most commonly used {@link ConfigDeprecation} + * when invoking a {@link ConfigDeprecationProvider}. + * + * See methods documentation for more detailed examples. + * + * @example + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename, unused }) => [ + * rename('oldKey', 'newKey'), + * unused('deprecatedKey'), + * ] + * ``` + * + * @public + */ +export interface ConfigDeprecationFactory { + /** + * Rename a configuration property from inside a plugin's configuration path. + * Will log a deprecation warning if the oldKey was found and deprecation applied. + * + * @example + * Rename 'myplugin.oldKey' to 'myplugin.newKey' + * ```typescript + * const provider: ConfigDeprecationProvider = ({ rename }) => [ + * rename('oldKey', 'newKey'), + * ] + * ``` + */ + rename(oldKey: string, newKey: string): ConfigDeprecation; + /** + * 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. + * + * @example + * Rename 'oldplugin.key' to 'newplugin.key' + * ```typescript + * const provider: ConfigDeprecationProvider = ({ renameFromRoot }) => [ + * renameFromRoot('oldplugin.key', 'newplugin.key'), + * ] + * ``` + */ + renameFromRoot(oldKey: string, newKey: string): 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. + * + * @example + * Flags 'myplugin.deprecatedKey' as unused + * ```typescript + * const provider: ConfigDeprecationProvider = ({ unused }) => [ + * unused('deprecatedKey'), + * ] + * ``` + */ + unused(unusedKey: string): ConfigDeprecation; + /** + * 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. + * + * @example + * Flags 'somepath.deprecatedProperty' as unused + * ```typescript + * const provider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ + * unusedFromRoot('somepath.deprecatedProperty'), + * ] + * ``` + */ + unusedFromRoot(unusedKey: string): ConfigDeprecation; +} + +/** @internal */ +export interface ConfigDeprecationWithContext { + deprecation: ConfigDeprecation; + path: string; +} diff --git a/src/core/server/config/index.ts b/src/core/server/config/index.ts index 491a24b2ab3d6f..04dc402d35b226 100644 --- a/src/core/server/config/index.ts +++ b/src/core/server/config/index.ts @@ -18,9 +18,16 @@ */ export { ConfigService, IConfigService } from './config_service'; -export { RawConfigService } from './raw_config_service'; +export { RawConfigService, RawConfigurationProvider } from './raw_config_service'; export { Config, ConfigPath, isConfigPath, hasConfigPathIntersection } from './config'; export { ObjectToConfigAdapter } from './object_to_config_adapter'; export { CliArgs, Env } from './env'; +export { + ConfigDeprecation, + ConfigDeprecationLogger, + ConfigDeprecationProvider, + ConfigDeprecationFactory, + coreDeprecationProvider, +} from './deprecation'; export { EnvironmentMode, PackageInfo } from './types'; diff --git a/src/legacy/server/config/deprecation_warnings.js b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts similarity index 70% rename from src/legacy/server/config/deprecation_warnings.js rename to src/core/server/config/integration_tests/config_deprecation.test.mocks.ts index 06cd3ba7cf037d..58b2da926b7c3b 100644 --- a/src/legacy/server/config/deprecation_warnings.js +++ b/src/core/server/config/integration_tests/config_deprecation.test.mocks.ts @@ -17,10 +17,9 @@ * under the License. */ -import { transformDeprecations } from './transform_deprecations'; - -export function configDeprecationWarningsMixin(kbnServer, server) { - transformDeprecations(kbnServer.settings, (message) => { - server.log(['warning', 'config', 'deprecation'], message); - }); -} +import { loggingServiceMock } from '../../logging/logging_service.mock'; +export const mockLoggingService = loggingServiceMock.create(); +mockLoggingService.asLoggerFactory.mockImplementation(() => mockLoggingService); +jest.doMock('../../logging/logging_service', () => ({ + LoggingService: jest.fn(() => mockLoggingService), +})); diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts new file mode 100644 index 00000000000000..e85f8567bfc683 --- /dev/null +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { mockLoggingService } from './config_deprecation.test.mocks'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import * as kbnTestServer from '../../../../test_utils/kbn_server'; + +describe('configuration deprecations', () => { + let root: ReturnType; + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + }); + + it('should not log deprecation warnings for default configuration', async () => { + root = kbnTestServer.createRoot(); + + await root.setup(); + + const logs = loggingServiceMock.collect(mockLoggingService); + expect(logs.warn).toMatchInlineSnapshot(`Array []`); + }); + + it('should log deprecation warnings for core deprecations', async () => { + root = kbnTestServer.createRoot({ + optimize: { + lazy: true, + lazyPort: 9090, + }, + }); + + await root.setup(); + + const logs = loggingServiceMock.collect(mockLoggingService); + expect(logs.warn).toMatchInlineSnapshot(` + Array [ + Array [ + "\\"optimize.lazy\\" is deprecated and has been replaced by \\"optimize.watch\\"", + ], + Array [ + "\\"optimize.lazyPort\\" is deprecated and has been replaced by \\"optimize.watchPort\\"", + ], + ] + `); + }); +}); diff --git a/src/legacy/ui/public/utils/ip_range.ts b/src/core/server/config/raw_config_service.mock.ts similarity index 57% rename from src/legacy/ui/public/utils/ip_range.ts rename to src/core/server/config/raw_config_service.mock.ts index 45ce21709d68c2..fdcb17395aaadd 100644 --- a/src/legacy/ui/public/utils/ip_range.ts +++ b/src/core/server/config/raw_config_service.mock.ts @@ -17,15 +17,23 @@ * under the License. */ -import { IpRangeKey } from '../agg_types/buckets/ip_range'; +import { RawConfigService } from './raw_config_service'; +import { Observable, of } from 'rxjs'; -export const ipRange = { - toString(range: IpRangeKey, format: (val: any) => string) { - if (range.type === 'mask') { - return format(range.mask); - } - const from = range.from ? format(range.from) : '-Infinity'; - const to = range.to ? format(range.to) : 'Infinity'; - return `${from} to ${to}`; - }, +const createRawConfigServiceMock = ({ + rawConfig = {}, + rawConfig$ = undefined, +}: { rawConfig?: Record; rawConfig$?: Observable> } = {}) => { + const mocked: jest.Mocked> = { + loadConfig: jest.fn(), + stop: jest.fn(), + reloadConfig: jest.fn(), + getConfig$: jest.fn().mockReturnValue(rawConfig$ || of(rawConfig)), + }; + + return mocked; +}; + +export const rawConfigServiceMock = { + create: createRawConfigServiceMock, }; diff --git a/src/core/server/config/raw_config_service.test.ts b/src/core/server/config/raw_config_service.test.ts index 361cef0d042ea9..f02c31d4659ca3 100644 --- a/src/core/server/config/raw_config_service.test.ts +++ b/src/core/server/config/raw_config_service.test.ts @@ -88,8 +88,8 @@ test('returns config at path as observable', async () => { .pipe(first()) .toPromise(); - expect(exampleConfig.get('key')).toEqual('value'); - expect(exampleConfig.getFlattenedPaths()).toEqual(['key']); + expect(exampleConfig.key).toEqual('value'); + expect(Object.keys(exampleConfig)).toEqual(['key']); }); test("pushes new configs when reloading even if config at path hasn't changed", async () => { @@ -110,19 +110,15 @@ test("pushes new configs when reloading even if config at path hasn't changed", configService.reloadConfig(); expect(valuesReceived).toMatchInlineSnapshot(` -Array [ - ObjectToConfigAdapter { - "rawConfig": Object { - "key": "value", - }, - }, - ObjectToConfigAdapter { - "rawConfig": Object { - "key": "value", - }, - }, -] -`); + Array [ + Object { + "key": "value", + }, + Object { + "key": "value", + }, + ] + `); }); test('pushes new config when reloading and config at path has changed', async () => { @@ -143,10 +139,10 @@ test('pushes new config when reloading and config at path has changed', async () configService.reloadConfig(); expect(valuesReceived).toHaveLength(2); - expect(valuesReceived[0].get('key')).toEqual('value'); - expect(valuesReceived[0].getFlattenedPaths()).toEqual(['key']); - expect(valuesReceived[1].get('key')).toEqual('new value'); - expect(valuesReceived[1].getFlattenedPaths()).toEqual(['key']); + expect(valuesReceived[0].key).toEqual('value'); + expect(Object.keys(valuesReceived[0])).toEqual(['key']); + expect(valuesReceived[1].key).toEqual('new value'); + expect(Object.keys(valuesReceived[1])).toEqual(['key']); }); test('completes config observables when stopped', done => { diff --git a/src/core/server/config/raw_config_service.ts b/src/core/server/config/raw_config_service.ts index b10137fb72f6ac..728d793f494a99 100644 --- a/src/core/server/config/raw_config_service.ts +++ b/src/core/server/config/raw_config_service.ts @@ -22,10 +22,12 @@ import { Observable, ReplaySubject } from 'rxjs'; import { map } from 'rxjs/operators'; import typeDetect from 'type-detect'; -import { Config } from './config'; -import { ObjectToConfigAdapter } from './object_to_config_adapter'; import { getConfigFromFiles } from './read_config'; +type RawConfigAdapter = (rawConfig: Record) => Record; + +export type RawConfigurationProvider = Pick; + /** @internal */ export class RawConfigService { /** @@ -35,12 +37,11 @@ export class RawConfigService { */ private readonly rawConfigFromFile$: ReplaySubject> = new ReplaySubject(1); - private readonly config$: Observable; + private readonly config$: Observable>; constructor( public readonly configFiles: readonly string[], - configAdapter: (rawConfig: Record) => Config = rawConfig => - new ObjectToConfigAdapter(rawConfig) + configAdapter: RawConfigAdapter = rawConfig => rawConfig ) { this.config$ = this.rawConfigFromFile$.pipe( map(rawConfig => { diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index eee80944172607..a2546709a318ca 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -24,7 +24,7 @@ import { BehaviorSubject } from 'rxjs'; import { HttpService } from '.'; import { HttpConfigType, config } from './http_config'; import { httpServerMock } from './http_server.mocks'; -import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { ConfigService, Env } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { contextServiceMock } from '../context/context_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; @@ -35,11 +35,12 @@ const coreId = Symbol(); const createConfigService = (value: Partial = {}) => { const configService = new ConfigService( - new BehaviorSubject( - new ObjectToConfigAdapter({ - server: value, - }) - ), + { + getConfig$: () => + new BehaviorSubject({ + server: value, + }), + }, env, logger ); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 57156322e2849b..c304958f78bb70 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -50,7 +50,16 @@ import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; export { bootstrap } from './bootstrap'; export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; -export { ConfigPath, ConfigService, EnvironmentMode, PackageInfo } from './config'; +export { + ConfigPath, + ConfigService, + ConfigDeprecation, + ConfigDeprecationProvider, + ConfigDeprecationLogger, + ConfigDeprecationFactory, + EnvironmentMode, + PackageInfo, +} from './config'; export { IContextContainer, IContextProvider, @@ -143,6 +152,7 @@ export { PluginInitializerContext, PluginManifest, PluginName, + SharedGlobalConfig, } from './plugins'; export { diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts index 2997a9c8e7affe..d8917b46eba62b 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.test.ts @@ -50,7 +50,7 @@ describe('ensureValidConfiguration', () => { coreHandledConfigPaths: ['core', 'elastic'], pluginSpecs: 'pluginSpecs', disabledPluginSpecs: 'disabledPluginSpecs', - inputSettings: 'settings', + settings: 'settings', legacyConfig: 'pluginExtendedConfig', }); }); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 8c76d45887761e..026683a7b7cb00 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -30,7 +30,7 @@ export async function ensureValidConfiguration( coreHandledConfigPaths: await configService.getUsedPaths(), pluginSpecs, disabledPluginSpecs, - inputSettings: settings, + settings, legacyConfig: pluginExtendedConfig, }); diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index 7b6be5368e7690..bf011fa01a3427 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -20,17 +20,10 @@ import { LegacyPluginSpec } from '../plugins/find_legacy_plugin_specs'; import { LegacyConfig } from './types'; import { getUnusedConfigKeys } from './get_unused_config_keys'; -// @ts-ignore -import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations'; - -jest.mock('../../../../legacy/server/config/transform_deprecations', () => ({ - transformDeprecations: jest.fn().mockImplementation(s => s), -})); describe('getUnusedConfigKeys', () => { beforeEach(() => { jest.resetAllMocks(); - transformDeprecations.mockImplementation((s: any) => s); }); const getConfig = (values: Record = {}): LegacyConfig => @@ -45,7 +38,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: {}, + settings: {}, legacyConfig: getConfig(), }) ).toEqual([]); @@ -57,7 +50,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, alsoInBoth: 'someValue', }, @@ -75,7 +68,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, }, legacyConfig: getConfig({ @@ -92,7 +85,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { presentInBoth: true, onlyInSetting: 'value', }, @@ -109,7 +102,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { elasticsearch: { username: 'foo', password: 'bar', @@ -131,7 +124,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { env: 'development', }, legacyConfig: getConfig({ @@ -149,7 +142,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: [], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { prop: ['a', 'b', 'c'], }, legacyConfig: getConfig({ @@ -171,7 +164,7 @@ describe('getUnusedConfigKeys', () => { getConfigPrefix: () => 'foo.bar', } as unknown) as LegacyPluginSpec, ], - inputSettings: { + settings: { foo: { bar: { unused: true, @@ -194,7 +187,7 @@ describe('getUnusedConfigKeys', () => { coreHandledConfigPaths: ['core', 'foo.bar'], pluginSpecs: [], disabledPluginSpecs: [], - inputSettings: { + settings: { core: { prop: 'value', }, @@ -209,46 +202,6 @@ describe('getUnusedConfigKeys', () => { }); describe('using deprecation', () => { - it('calls transformDeprecations with the settings', async () => { - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], - inputSettings: { - prop: 'settings', - }, - legacyConfig: getConfig({ - prop: 'config', - }), - }); - expect(transformDeprecations).toHaveBeenCalledTimes(1); - expect(transformDeprecations).toHaveBeenCalledWith({ - prop: 'settings', - }); - }); - - it('uses the transformed settings', async () => { - transformDeprecations.mockImplementation((settings: Record) => { - delete settings.deprecated; - settings.updated = 'new value'; - return settings; - }); - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], - inputSettings: { - onlyInSettings: 'bar', - deprecated: 'value', - }, - legacyConfig: getConfig({ - updated: 'config', - }), - }) - ).toEqual(['onlyInSettings']); - }); - it('should use the plugin deprecations provider', async () => { expect( await getUnusedConfigKeys({ @@ -262,7 +215,7 @@ describe('getUnusedConfigKeys', () => { } as unknown) as LegacyPluginSpec, ], disabledPluginSpecs: [], - inputSettings: { + settings: { foo: { foo: 'dolly', foo1: 'bar', diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 22e7c50c0bc25e..73cc7d8c50474c 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -19,8 +19,6 @@ import { difference, get, set } from 'lodash'; // @ts-ignore -import { transformDeprecations } from '../../../../legacy/server/config/transform_deprecations'; -// @ts-ignore import { getTransform } from '../../../../legacy/deprecation/index'; import { unset, getFlattenedObject } from '../../../../legacy/utils'; import { hasConfigPathIntersection } from '../../config'; @@ -33,18 +31,15 @@ export async function getUnusedConfigKeys({ coreHandledConfigPaths, pluginSpecs, disabledPluginSpecs, - inputSettings, + settings, legacyConfig, }: { coreHandledConfigPaths: string[]; pluginSpecs: LegacyPluginSpec[]; disabledPluginSpecs: LegacyPluginSpec[]; - inputSettings: Record; + settings: Record; legacyConfig: LegacyConfig; }) { - // transform deprecated core settings - const settings = transformDeprecations(inputSettings); - // transform deprecated plugin settings for (let i = 0; i < pluginSpecs.length; i++) { const spec = pluginSpecs[i]; diff --git a/src/core/server/legacy/config/index.ts b/src/core/server/legacy/config/index.ts index a837b8639939eb..c3f308fd6d903b 100644 --- a/src/core/server/legacy/config/index.ts +++ b/src/core/server/legacy/config/index.ts @@ -19,4 +19,10 @@ export { ensureValidConfiguration } from './ensure_valid_configuration'; export { LegacyObjectToConfigAdapter } from './legacy_object_to_config_adapter'; -export { LegacyConfig } from './types'; +export { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; +export { + LegacyConfig, + LegacyConfigDeprecation, + LegacyConfigDeprecationFactory, + LegacyConfigDeprecationProvider, +} from './types'; diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts new file mode 100644 index 00000000000000..144e057c118f70 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { convertLegacyDeprecationProvider } from './legacy_deprecation_adapters'; +import { LegacyConfigDeprecationProvider } from './types'; +import { ConfigDeprecation } from '../../config'; +import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; +import { applyDeprecations } from '../../config/deprecation/apply_deprecations'; + +jest.spyOn(configDeprecationFactory, 'unusedFromRoot'); +jest.spyOn(configDeprecationFactory, 'renameFromRoot'); + +const executeHandlers = (handlers: ConfigDeprecation[]) => { + handlers.forEach(handler => { + handler({}, '', () => null); + }); +}; + +describe('convertLegacyDeprecationProvider', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the same number of handlers', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + unused('d'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + expect(handlers).toHaveLength(3); + }); + + it('invokes the factory "unusedFromRoot" when using legacy "unused"', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + unused('d'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + executeHandlers(handlers); + + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledTimes(2); + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('c'); + expect(configDeprecationFactory.unusedFromRoot).toHaveBeenCalledWith('d'); + }); + + it('invokes the factory "renameFromRoot" when using legacy "rename"', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('a', 'b'), + unused('c'), + rename('d', 'e'), + ]; + + const migrated = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = migrated(configDeprecationFactory); + executeHandlers(handlers); + + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledTimes(2); + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('a', 'b'); + expect(configDeprecationFactory.renameFromRoot).toHaveBeenCalledWith('d', 'e'); + }); + + it('properly works in a real use case', async () => { + const legacyProvider: LegacyConfigDeprecationProvider = ({ rename, unused }) => [ + rename('old', 'new'), + unused('unused'), + unused('notpresent'), + ]; + + const convertedProvider = await convertLegacyDeprecationProvider(legacyProvider); + const handlers = convertedProvider(configDeprecationFactory); + + const rawConfig = { + old: 'oldvalue', + unused: 'unused', + goodValue: 'good', + }; + + const migrated = applyDeprecations( + rawConfig, + handlers.map(handler => ({ deprecation: handler, path: '' })) + ); + expect(migrated).toEqual({ new: 'oldvalue', goodValue: 'good' }); + }); +}); diff --git a/src/core/server/legacy/config/legacy_deprecation_adapters.ts b/src/core/server/legacy/config/legacy_deprecation_adapters.ts new file mode 100644 index 00000000000000..b0e3bc37e15107 --- /dev/null +++ b/src/core/server/legacy/config/legacy_deprecation_adapters.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ConfigDeprecation, ConfigDeprecationProvider } from '../../config/deprecation'; +import { LegacyConfigDeprecation, LegacyConfigDeprecationProvider } from './index'; +import { configDeprecationFactory } from '../../config/deprecation/deprecation_factory'; + +const convertLegacyDeprecation = ( + legacyDeprecation: LegacyConfigDeprecation +): ConfigDeprecation => (config, fromPath, logger) => { + legacyDeprecation(config, logger); + return config; +}; + +const legacyUnused = (unusedKey: string): LegacyConfigDeprecation => (settings, log) => { + const deprecation = configDeprecationFactory.unusedFromRoot(unusedKey); + deprecation(settings, '', log); +}; + +const legacyRename = (oldKey: string, newKey: string): LegacyConfigDeprecation => ( + settings, + log +) => { + const deprecation = configDeprecationFactory.renameFromRoot(oldKey, newKey); + deprecation(settings, '', log); +}; + +/** + * Async deprecation provider converter for legacy deprecation implementation + * + * @internal + */ +export const convertLegacyDeprecationProvider = async ( + legacyProvider: LegacyConfigDeprecationProvider +): Promise => { + const legacyDeprecations = await legacyProvider({ + rename: legacyRename, + unused: legacyUnused, + }); + return () => legacyDeprecations.map(convertLegacyDeprecation); +}; 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 8035596bb6072b..75e1813f8c1f66 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 @@ -17,7 +17,8 @@ * under the License. */ -import { ConfigPath, ObjectToConfigAdapter } from '../../config'; +import { ConfigPath } from '../../config'; +import { ObjectToConfigAdapter } from '../../config/object_to_config_adapter'; /** * Represents logging config supported by the legacy platform. diff --git a/src/core/server/legacy/config/types.ts b/src/core/server/legacy/config/types.ts index cc4a6ac11fee40..24869e361c39c2 100644 --- a/src/core/server/legacy/config/types.ts +++ b/src/core/server/legacy/config/types.ts @@ -26,3 +26,33 @@ export interface LegacyConfig { get(key?: string): T; has(key: string): boolean; } + +/** + * Representation of a legacy configuration deprecation factory used for + * legacy plugin deprecations. + * + * @internal + */ +export interface LegacyConfigDeprecationFactory { + rename(oldKey: string, newKey: string): LegacyConfigDeprecation; + unused(unusedKey: string): LegacyConfigDeprecation; +} + +/** + * Representation of a legacy configuration deprecation. + * + * @internal + */ +export type LegacyConfigDeprecation = ( + settings: Record, + log: (msg: string) => void +) => void; + +/** + * Representation of a legacy configuration deprecation provider. + * + * @internal + */ +export type LegacyConfigDeprecationProvider = ( + factory: LegacyConfigDeprecationFactory +) => LegacyConfigDeprecation[] | Promise; diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 73b7ced60ee49a..d4360c577d24c1 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -21,13 +21,9 @@ import { BehaviorSubject, throwError } from 'rxjs'; jest.mock('../../../legacy/server/kbn_server'); jest.mock('../../../cli/cluster/cluster_manager'); -jest.mock('./plugins/find_legacy_plugin_specs.ts', () => ({ - findLegacyPluginSpecs: (settings: Record) => ({ - pluginSpecs: [], - pluginExtendedConfig: settings, - disabledPluginSpecs: [], - uiExports: [], - }), +jest.mock('./plugins/find_legacy_plugin_specs'); +jest.mock('./config/legacy_deprecation_adapters', () => ({ + convertLegacyDeprecationProvider: (provider: any) => Promise.resolve(provider), })); import { LegacyService, LegacyServiceSetupDeps, LegacyServiceStartDeps } from '.'; @@ -47,8 +43,10 @@ import { httpServiceMock } from '../http/http_service.mock'; import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock'; import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; +import { findLegacyPluginSpecs } from './plugins/find_legacy_plugin_specs'; const MockKbnServer: jest.Mock = KbnServer as any; +const findLegacyPluginSpecsMock: jest.Mock = findLegacyPluginSpecs as any; let coreId: symbol; let env: Env; @@ -66,6 +64,16 @@ beforeEach(() => { env = Env.createDefault(getEnvOptions()); configService = configServiceMock.create(); + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs: [], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }) as any + ); + MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); setupDeps = { @@ -115,6 +123,7 @@ beforeEach(() => { afterEach(() => { jest.clearAllMocks(); + findLegacyPluginSpecsMock.mockReset(); }); describe('once LegacyService is set up with connection info', () => { @@ -382,3 +391,52 @@ test('Cannot start without setup phase', async () => { `"Legacy service is not setup yet."` ); }); + +describe('#discoverPlugins()', () => { + it('calls findLegacyPluginSpecs with correct parameters', async () => { + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1); + expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger); + }); + + it(`register legacy plugin's deprecation providers`, async () => { + findLegacyPluginSpecsMock.mockImplementation( + settings => + Promise.resolve({ + pluginSpecs: [ + { + getDeprecationsProvider: () => undefined, + }, + { + getDeprecationsProvider: () => 'providerA', + }, + { + getDeprecationsProvider: () => 'providerB', + }, + ], + pluginExtendedConfig: settings, + disabledPluginSpecs: [], + uiExports: [], + }) as any + ); + + const legacyService = new LegacyService({ + coreId, + env, + logger, + configService: configService as any, + }); + + await legacyService.discoverPlugins(); + expect(configService.addDeprecationProvider).toHaveBeenCalledTimes(2); + expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerA'); + expect(configService.addDeprecationProvider).toHaveBeenCalledWith('', 'providerB'); + }); +}); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index fcf0c45c17db87..4c2e57dc69b29c 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -23,7 +23,7 @@ import { CoreService } from '../../types'; import { CoreSetup, CoreStart } from '../'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { SavedObjectsLegacyUiExports } from '../types'; -import { Config } from '../config'; +import { Config, ConfigDeprecationProvider } from '../config'; import { CoreContext } from '../core_context'; import { DevConfig, DevConfigType } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType } from '../http'; @@ -32,7 +32,7 @@ import { PluginsServiceSetup, PluginsServiceStart } from '../plugins'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyPluginSpec } from './plugins/find_legacy_plugin_specs'; import { PathConfigType } from '../path'; -import { LegacyConfig } from './config'; +import { LegacyConfig, convertLegacyDeprecationProvider } from './config'; interface LegacyKbnServer { applyLoggingConfiguration: (settings: Readonly>) => void; @@ -157,6 +157,18 @@ export class LegacyService implements CoreService { uiExports, }; + const deprecationProviders = await pluginSpecs + .map(spec => spec.getDeprecationsProvider()) + .reduce(async (providers, current) => { + if (current) { + return [...(await providers), await convertLegacyDeprecationProvider(current)]; + } + return providers; + }, Promise.resolve([] as ConfigDeprecationProvider[])); + deprecationProviders.forEach(provider => + this.coreContext.configService.addDeprecationProvider('', provider) + ); + this.legacyRawConfig = pluginExtendedConfig; // check for unknown uiExport types diff --git a/src/core/server/legacy/logging/legacy_logging_server.ts b/src/core/server/legacy/logging/legacy_logging_server.ts index 0fe305fe77471a..57706bcac2232f 100644 --- a/src/core/server/legacy/logging/legacy_logging_server.ts +++ b/src/core/server/legacy/logging/legacy_logging_server.ts @@ -20,7 +20,7 @@ import { ServerExtType } from 'hapi'; import Podium from 'podium'; // @ts-ignore: implicit any for JS file -import { Config, transformDeprecations } from '../../../../legacy/server/config'; +import { Config } from '../../../../legacy/server/config'; // @ts-ignore: implicit any for JS file import { setupLogging } from '../../../../legacy/server/logging'; import { LogLevel } from '../../logging/log_level'; @@ -99,7 +99,7 @@ export class LegacyLoggingServer { ops: { interval: 2147483647 }, }; - setupLogging(this, Config.withDefaultSchema(transformDeprecations(config))); + setupLogging(this, Config.withDefaultSchema(config)); } public register({ plugin: { register }, options }: PluginRegisterParams): Promise { diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts index 08ec1b004d7b43..0a49154801e563 100644 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts @@ -27,7 +27,7 @@ import { import { LoggerFactory } from '../../logging'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { collectUiExports as collectLegacyUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; -import { LegacyConfig } from '../config'; +import { LegacyConfig, LegacyConfigDeprecationProvider } from '../config'; export interface LegacyPluginPack { getPath(): string; @@ -37,6 +37,7 @@ export interface LegacyPluginSpec { getId: () => unknown; getExpectedKibanaVersion: () => string; getConfigPrefix: () => string; + getDeprecationsProvider: () => LegacyConfigDeprecationProvider | undefined; } export async function findLegacyPluginSpecs(settings: unknown, loggerFactory: LoggerFactory) { diff --git a/src/core/server/logging/logging_service.mock.ts b/src/core/server/logging/logging_service.mock.ts index b5f522ca36a5f0..50e6edc227bb56 100644 --- a/src/core/server/logging/logging_service.mock.ts +++ b/src/core/server/logging/logging_service.mock.ts @@ -45,7 +45,7 @@ const createLoggingServiceMock = () => { context, ...mockLog, })); - mocked.asLoggerFactory.mockImplementation(() => createLoggingServiceMock()); + mocked.asLoggerFactory.mockImplementation(() => mocked); mocked.stop.mockResolvedValue(); return mocked; }; diff --git a/src/core/server/plugins/discovery/plugins_discovery.test.ts b/src/core/server/plugins/discovery/plugins_discovery.test.ts index 224259bc121ec7..bf55fc7caae4ca 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.test.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.test.ts @@ -20,9 +20,9 @@ import { mockPackage, mockReaddir, mockReadFile, mockStat } from './plugins_discovery.test.mocks'; import { resolve } from 'path'; -import { BehaviorSubject } from 'rxjs'; import { first, map, toArray } from 'rxjs/operators'; -import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../../config'; +import { ConfigService, Env } from '../../config'; +import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; import { getEnvOptions } from '../../config/__mocks__/env'; import { loggingServiceMock } from '../../logging/logging_service.mock'; import { PluginWrapper } from '../plugin'; @@ -115,9 +115,7 @@ test('properly iterates through plugin search locations', async () => { }) ); const configService = new ConfigService( - new BehaviorSubject( - new ObjectToConfigAdapter({ plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } }) - ), + rawConfigServiceMock.create({ rawConfig: { plugins: { paths: [TEST_EXTRA_PLUGIN_PATH] } } }), env, logger ); diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts index 547ac08cca76db..3fcd7fbbbe1ff9 100644 --- a/src/core/server/plugins/plugin_context.test.ts +++ b/src/core/server/plugins/plugin_context.test.ts @@ -18,12 +18,12 @@ */ import { duration } from 'moment'; -import { BehaviorSubject } from 'rxjs'; import { first } from 'rxjs/operators'; import { createPluginInitializerContext } from './plugin_context'; import { CoreContext } from '../core_context'; -import { Env, ObjectToConfigAdapter } from '../config'; +import { Env } from '../config'; import { loggingServiceMock } from '../logging/logging_service.mock'; +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { PluginManifest } from './types'; import { Server } from '../server'; @@ -54,9 +54,9 @@ describe('Plugin Context', () => { beforeEach(async () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); + const config$ = rawConfigServiceMock.create({ rawConfig: {} }); server = new Server(config$, env, logger); - await server.setupConfigSchemas(); + await server.setupCoreConfig(); coreContext = { coreId, env, logger, configService: server.configService }; }); diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index df5473bc97d994..6768e85c8db17e 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -23,7 +23,8 @@ import { resolve, join } from 'path'; import { BehaviorSubject, from } from 'rxjs'; import { schema } from '@kbn/config-schema'; -import { Config, ConfigPath, ConfigService, Env, ObjectToConfigAdapter } from '../config'; +import { ConfigPath, ConfigService, Env } from '../config'; +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; import { getEnvOptions } from '../config/__mocks__/env'; import { coreMock } from '../mocks'; import { loggingServiceMock } from '../logging/logging_service.mock'; @@ -38,7 +39,7 @@ import { DiscoveredPlugin } from './types'; const MockPluginsSystem: jest.Mock = PluginsSystem as any; let pluginsService: PluginsService; -let config$: BehaviorSubject; +let config$: BehaviorSubject>; let configService: ConfigService; let coreId: symbol; let env: Env; @@ -109,10 +110,9 @@ describe('PluginsService', () => { coreId = Symbol('core'); env = Env.createDefault(getEnvOptions()); - config$ = new BehaviorSubject( - new ObjectToConfigAdapter({ plugins: { initialize: true } }) - ); - configService = new ConfigService(config$, env, logger); + config$ = new BehaviorSubject>({ plugins: { initialize: true } }); + const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); + configService = new ConfigService(rawConfigService, env, logger); await configService.setSchema(config.path, config.schema); pluginsService = new PluginsService({ coreId, env, logger, configService }); @@ -388,6 +388,40 @@ describe('PluginsService', () => { await pluginsService.discover(); expect(configService.setSchema).toBeCalledWith('path', configSchema); }); + + it('registers plugin config deprecation provider in config service', async () => { + const configSchema = schema.string(); + jest.spyOn(configService, 'setSchema').mockImplementation(() => Promise.resolve()); + jest.spyOn(configService, 'addDeprecationProvider'); + + const deprecationProvider = () => []; + jest.doMock( + join('path-with-provider', 'server'), + () => ({ + config: { + schema: configSchema, + deprecations: deprecationProvider, + }, + }), + { + virtual: true, + } + ); + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('some-id', { + path: 'path-with-provider', + configPath: 'config-path', + }), + ]), + }); + await pluginsService.discover(); + expect(configService.addDeprecationProvider).toBeCalledWith( + 'config-path', + deprecationProvider + ); + }); }); describe('#generateUiPluginsConfigs()', () => { @@ -499,9 +533,7 @@ describe('PluginsService', () => { mockPluginSystem.uiPlugins.mockReturnValue(new Map()); - config$.next( - new ObjectToConfigAdapter({ plugins: { initialize: true }, plugin1: { enabled: false } }) - ); + config$.next({ plugins: { initialize: true }, plugin1: { enabled: false } }); await pluginsService.discover(); const { uiPlugins } = await pluginsService.setup({} as any); diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index 3f9999aad4ab92..5a50cf8ea8ba2c 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -196,6 +196,12 @@ export class PluginsService implements CoreService = Type; /** - * Describes a plugin configuration schema and capabilities. + * Describes a plugin configuration properties. * * @example * ```typescript @@ -56,12 +56,20 @@ export type PluginConfigSchema = Type; * uiProp: true, * }, * schema: configSchema, + * deprecations: ({ rename, unused }) => [ + * rename('securityKey', 'secret'), + * unused('deprecatedProperty'), + * ], * }; * ``` * * @public */ export interface PluginConfigDescriptor { + /** + * Provider for the {@link ConfigDeprecation} to apply to the plugin configuration. + */ + deprecations?: ConfigDeprecationProvider; /** * List of configuration properties that will be available on the client-side plugin. */ @@ -206,6 +214,9 @@ export const SharedGlobalConfigKeys = { path: ['data'] as const, }; +/** + * @public + */ export type SharedGlobalConfig = RecursiveReadonly<{ kibana: Pick; elasticsearch: Pick; diff --git a/src/core/server/root/index.test.mocks.ts b/src/core/server/root/index.test.mocks.ts index 5754e5a5b9321d..1d3add66d7c22c 100644 --- a/src/core/server/root/index.test.mocks.ts +++ b/src/core/server/root/index.test.mocks.ts @@ -29,8 +29,14 @@ jest.doMock('../config/config_service', () => ({ ConfigService: jest.fn(() => configService), })); +import { rawConfigServiceMock } from '../config/raw_config_service.mock'; +export const rawConfigService = rawConfigServiceMock.create(); +jest.doMock('../config/raw_config_service', () => ({ + RawConfigService: jest.fn(() => rawConfigService), +})); + export const mockServer = { - setupConfigSchemas: jest.fn(), + setupCoreConfig: jest.fn(), setup: jest.fn(), stop: jest.fn(), configService, diff --git a/src/core/server/root/index.test.ts b/src/core/server/root/index.test.ts index 4eba2133dce285..3b187aac022c30 100644 --- a/src/core/server/root/index.test.ts +++ b/src/core/server/root/index.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { configService, logger, mockServer } from './index.test.mocks'; +import { rawConfigService, configService, logger, mockServer } from './index.test.mocks'; import { BehaviorSubject } from 'rxjs'; import { filter, first } from 'rxjs/operators'; @@ -26,13 +26,13 @@ import { Env } from '../config'; import { getEnvOptions } from '../config/__mocks__/env'; const env = new Env('.', getEnvOptions()); -const config$ = configService.getConfig$(); let mockConsoleError: jest.SpyInstance; beforeEach(() => { jest.spyOn(global.process, 'exit').mockReturnValue(undefined as never); mockConsoleError = jest.spyOn(console, 'error').mockReturnValue(undefined); + rawConfigService.getConfig$.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); configService.atPath.mockReturnValue(new BehaviorSubject({ someValue: 'foo' })); }); @@ -40,7 +40,7 @@ afterEach(() => { jest.restoreAllMocks(); logger.asLoggerFactory.mockClear(); logger.stop.mockClear(); - configService.getConfig$.mockClear(); + rawConfigService.getConfig$.mockClear(); logger.upgrade.mockReset(); configService.atPath.mockReset(); @@ -49,7 +49,7 @@ afterEach(() => { }); test('sets up services on "setup"', async () => { - const root = new Root(config$, env); + const root = new Root(rawConfigService, env); expect(logger.upgrade).not.toHaveBeenCalled(); expect(mockServer.setup).not.toHaveBeenCalled(); @@ -65,7 +65,7 @@ test('upgrades logging configuration after setup', async () => { const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' }); configService.atPath.mockReturnValue(mockLoggingConfig$); - const root = new Root(config$, env); + const root = new Root(rawConfigService, env); await root.setup(); expect(logger.upgrade).toHaveBeenCalledTimes(1); @@ -80,7 +80,7 @@ test('upgrades logging configuration after setup', async () => { test('stops services on "shutdown"', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); @@ -98,7 +98,7 @@ test('stops services on "shutdown"', async () => { test('stops services on "shutdown" an calls `onShutdown` with error passed to `shutdown`', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); @@ -117,7 +117,7 @@ test('stops services on "shutdown" an calls `onShutdown` with error passed to `s test('fails and stops services if server setup fails', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); const serverError = new Error('server failed'); mockServer.setup.mockRejectedValue(serverError); @@ -136,7 +136,7 @@ test('fails and stops services if server setup fails', async () => { test('fails and stops services if initial logger upgrade fails', async () => { const mockOnShutdown = jest.fn(); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); const loggingUpgradeError = new Error('logging config upgrade failed'); logger.upgrade.mockImplementation(() => { @@ -167,7 +167,7 @@ test('stops services if consequent logger upgrade fails', async () => { const mockLoggingConfig$ = new BehaviorSubject({ someValue: 'foo' }); configService.atPath.mockReturnValue(mockLoggingConfig$); - const root = new Root(config$, env, mockOnShutdown); + const root = new Root(rawConfigService, env, mockOnShutdown); await root.setup(); expect(mockOnShutdown).not.toHaveBeenCalled(); diff --git a/src/core/server/root/index.ts b/src/core/server/root/index.ts index ac6ef79483280e..eecc6399366dcd 100644 --- a/src/core/server/root/index.ts +++ b/src/core/server/root/index.ts @@ -17,10 +17,10 @@ * under the License. */ -import { ConnectableObservable, Observable, Subscription } from 'rxjs'; +import { ConnectableObservable, Subscription } from 'rxjs'; import { first, map, publishReplay, switchMap, tap } from 'rxjs/operators'; -import { Config, Env } from '../config'; +import { Env, RawConfigurationProvider } from '../config'; import { Logger, LoggerFactory, LoggingConfigType, LoggingService } from '../logging'; import { Server } from '../server'; @@ -35,19 +35,19 @@ export class Root { private loggingConfigSubscription?: Subscription; constructor( - config$: Observable, + rawConfigProvider: RawConfigurationProvider, env: Env, private readonly onShutdown?: (reason?: Error | string) => void ) { this.loggingService = new LoggingService(); this.logger = this.loggingService.asLoggerFactory(); this.log = this.logger.get('root'); - this.server = new Server(config$, env, this.logger); + this.server = new Server(rawConfigProvider, env, this.logger); } public async setup() { try { - await this.server.setupConfigSchemas(); + await this.server.setupCoreConfig(); await this.setupLogging(); this.log.debug('setting up root'); return await this.server.setup(); diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 2c6f5e4a520a7f..c1fa820ef2019d 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -23,6 +23,16 @@ import { MigrationDefinition } from './migrations/core/document_migrator'; import { SavedObjectsSchemaDefinition } from './schema'; import { PropertyValidators } from './validation'; +export { + SavedObjectsImportResponse, + SavedObjectsImportConflictError, + SavedObjectsImportUnsupportedTypeError, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportUnknownError, + SavedObjectsImportError, + SavedObjectsImportRetry, +} from './import/types'; + /** * Information about the migrations that have been applied to this SavedObject. * When Kibana starts up, KibanaMigrator detects outdated documents and diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c855e04e420f75..18e76324ff3093 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -502,15 +502,34 @@ export class ClusterClient implements IClusterClient { close(): void; } +// @public +export type ConfigDeprecation = (config: Record, fromPath: string, logger: ConfigDeprecationLogger) => Record; + +// @public +export interface ConfigDeprecationFactory { + rename(oldKey: string, newKey: string): ConfigDeprecation; + renameFromRoot(oldKey: string, newKey: string): ConfigDeprecation; + unused(unusedKey: string): ConfigDeprecation; + unusedFromRoot(unusedKey: string): ConfigDeprecation; +} + +// @public +export type ConfigDeprecationLogger = (message: string) => void; + +// @public +export type ConfigDeprecationProvider = (factory: ConfigDeprecationFactory) => ConfigDeprecation[]; + // @public (undocumented) export type ConfigPath = string | string[]; // @internal (undocumented) export class ConfigService { - // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "RawConfigurationProvider" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Env" needs to be exported by the entry point index.d.ts - constructor(config$: Observable, env: Env, logger: LoggerFactory); + constructor(rawConfigProvider: RawConfigurationProvider, env: Env, logger: LoggerFactory); + addDeprecationProvider(path: ConfigPath, provider: ConfigDeprecationProvider): void; atPath(path: ConfigPath): Observable; + // Warning: (ae-forgotten-export) The symbol "Config" needs to be exported by the entry point index.d.ts getConfig$(): Observable; // (undocumented) getUnusedPaths(): Promise; @@ -520,6 +539,7 @@ export class ConfigService { isEnabledAtPath(path: ConfigPath): Promise; optionalAtPath(path: ConfigPath): Observable; setSchema(path: ConfigPath, schema: Type): Promise; + validate(): Promise; } // @public @@ -1024,6 +1044,7 @@ export interface Plugin { + deprecations?: ConfigDeprecationProvider; exposeToBrowser?: { [P in keyof T]?: boolean; }; @@ -1748,6 +1769,13 @@ export interface SessionStorageFactory { asScoped: (request: KibanaRequest) => SessionStorage; } +// @public (undocumented) +export type SharedGlobalConfig = RecursiveReadonly_2<{ + kibana: Pick; + elasticsearch: Pick; + path: Pick; +}>; + // @public export interface UiSettingsParams { category?: string[]; @@ -1785,6 +1813,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:228:15 - (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:222:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:223:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 7b91fb7257957c..d593a6275fa4c6 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -29,14 +29,16 @@ import { } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; -import { Env, Config, ObjectToConfigAdapter } from './config'; +import { Env } from './config'; import { Server } from './server'; import { getEnvOptions } from './config/__mocks__/env'; import { loggingServiceMock } from './logging/logging_service.mock'; +import { rawConfigServiceMock } from './config/raw_config_service.mock'; const env = new Env('.', getEnvOptions()); const logger = loggingServiceMock.create(); +const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); @@ -47,9 +49,8 @@ afterEach(() => { jest.clearAllMocks(); }); -const config$ = new BehaviorSubject(new ObjectToConfigAdapter({})); test('sets up services on "setup"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); @@ -67,7 +68,7 @@ test('sets up services on "setup"', async () => { }); test('injects legacy dependency to context#setup()', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); const pluginA = Symbol(); const pluginB = Symbol(); @@ -89,7 +90,7 @@ test('injects legacy dependency to context#setup()', async () => { }); test('runs services on "start"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.start).not.toHaveBeenCalled(); @@ -109,13 +110,13 @@ test('runs services on "start"', async () => { test('does not fail on "setup" if there are unused paths detected', async () => { mockConfigService.getUnusedPaths.mockResolvedValue(['some.path', 'another.path']); - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await expect(server.setup()).resolves.toBeDefined(); }); test('stops services on "stop"', async () => { - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await server.setup(); @@ -134,26 +135,25 @@ test('stops services on "stop"', async () => { expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); }); -test(`doesn't setup core services if services config validation fails`, async () => { - mockConfigService.setSchema.mockImplementation(() => { - throw new Error('invalid config'); +test(`doesn't setup core services if config validation fails`, async () => { + mockConfigService.validate.mockImplementationOnce(() => { + return Promise.reject(new Error('invalid config')); }); - const server = new Server(config$, env, logger); - await expect(server.setupConfigSchemas()).rejects.toThrowErrorMatchingInlineSnapshot( - `"invalid config"` - ); + const server = new Server(rawConfigService, env, logger); + await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot(`"invalid config"`); + expect(mockHttpService.setup).not.toHaveBeenCalled(); expect(mockElasticsearchService.setup).not.toHaveBeenCalled(); expect(mockPluginsService.setup).not.toHaveBeenCalled(); expect(mockLegacyService.setup).not.toHaveBeenCalled(); }); -test(`doesn't setup core services if config validation fails`, async () => { +test(`doesn't setup core services if legacy config validation fails`, async () => { mockEnsureValidConfiguration.mockImplementation(() => { throw new Error('Unknown configuration keys'); }); - const server = new Server(config$, env, logger); + const server = new Server(rawConfigService, env, logger); await expect(server.setup()).rejects.toThrowErrorMatchingInlineSnapshot( `"Unknown configuration keys"` diff --git a/src/core/server/server.ts b/src/core/server/server.ts index e7166f30caa346..e7bc57ea5fb948 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -16,11 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import { Observable } from 'rxjs'; + import { take } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; -import { ConfigService, Env, Config, ConfigPath } from './config'; +import { + ConfigService, + Env, + ConfigPath, + RawConfigurationProvider, + coreDeprecationProvider, +} from './config'; import { ElasticsearchService } from './elasticsearch'; import { HttpService, InternalHttpServiceSetup } from './http'; import { LegacyService, ensureValidConfiguration } from './legacy'; @@ -44,6 +50,7 @@ import { InternalCoreSetup } from './internal_types'; import { CapabilitiesService } from './capabilities'; const coreId = Symbol('core'); +const rootConfigPath = ''; export class Server { public readonly configService: ConfigService; @@ -58,12 +65,12 @@ export class Server { private readonly uiSettings: UiSettingsService; constructor( - public readonly config$: Observable, + rawConfigProvider: RawConfigurationProvider, public readonly env: Env, private readonly logger: LoggerFactory ) { this.log = this.logger.get('server'); - this.configService = new ConfigService(config$, env, logger); + this.configService = new ConfigService(rawConfigProvider, env, logger); const core = { coreId, configService: this.configService, env, logger }; this.context = new ContextService(core); @@ -84,6 +91,7 @@ export class Server { const legacyPlugins = await this.legacy.discoverPlugins(); // Immediately terminate in case of invalid configuration + await this.configService.validate(); await ensureValidConfiguration(this.configService, legacyPlugins); const contextServiceSetup = this.context.setup({ @@ -207,7 +215,7 @@ export class Server { ); } - public async setupConfigSchemas() { + public async setupCoreConfig() { const schemas: Array<[ConfigPath, Type]> = [ [pathConfig.path, pathConfig.schema], [elasticsearchConfig.path, elasticsearchConfig.schema], @@ -220,6 +228,8 @@ export class Server { [uiSettingsConfig.path, uiSettingsConfig.schema], ]; + this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); + for (const [path, schema] of schemas) { await this.configService.setSchema(path, schema); } diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 98f0800feae79a..b51cc4ef56410e 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -25,3 +25,4 @@ export * from './map_to_object'; export * from './merge'; export * from './pick'; export * from './url'; +export * from './unset'; diff --git a/src/core/utils/unset.test.ts b/src/core/utils/unset.test.ts new file mode 100644 index 00000000000000..c0112e729811f2 --- /dev/null +++ b/src/core/utils/unset.test.ts @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { unset } from './unset'; + +describe('unset', () => { + it('deletes a property from an object', () => { + const obj = { + a: 'a', + b: 'b', + c: 'c', + }; + unset(obj, 'a'); + expect(obj).toEqual({ + b: 'b', + c: 'c', + }); + }); + + it('does nothing if the property is not present', () => { + const obj = { + a: 'a', + b: 'b', + c: 'c', + }; + unset(obj, 'd'); + expect(obj).toEqual({ + a: 'a', + b: 'b', + c: 'c', + }); + }); + + it('handles nested paths', () => { + const obj = { + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }; + unset(obj, 'foo.bar.one'); + expect(obj).toEqual({ + foo: { + bar: { + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }); + }); + + it('does nothing if nested paths does not exist', () => { + const obj = { + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }; + unset(obj, 'foo.nothere.baz'); + expect(obj).toEqual({ + foo: { + bar: { + one: 'one', + two: 'two', + }, + hello: 'dolly', + }, + some: { + things: 'here', + }, + }); + }); +}); diff --git a/src/core/utils/unset.ts b/src/core/utils/unset.ts new file mode 100644 index 00000000000000..8008d4ee08ba36 --- /dev/null +++ b/src/core/utils/unset.ts @@ -0,0 +1,49 @@ +/* + * 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 { get } from './get'; + +/** + * Unset a (potentially nested) key from given object. + * This mutates the original object. + * + * @example + * ``` + * unset(myObj, 'someRootProperty'); + * unset(myObj, 'some.nested.path'); + * ``` + */ +export function unset(obj: OBJ, atPath: string) { + const paths = atPath + .split('.') + .map(s => s.trim()) + .filter(v => v !== ''); + if (paths.length === 0) { + return; + } + if (paths.length === 1) { + delete obj[paths[0]]; + return; + } + const property = paths.pop() as string; + const parent = get(obj, paths as any) as any; + if (parent !== undefined) { + delete parent[property]; + } +} diff --git a/src/dev/jest/integration_tests/junit_reporter.test.js b/src/dev/jest/integration_tests/junit_reporter.test.js index ed5d73cd87c40f..2abfa5648dcca9 100644 --- a/src/dev/jest/integration_tests/junit_reporter.test.js +++ b/src/dev/jest/integration_tests/junit_reporter.test.js @@ -24,12 +24,13 @@ import { readFileSync } from 'fs'; import del from 'del'; import execa from 'execa'; import xml2js from 'xml2js'; +import { makeJunitReportPath } from '@kbn/test'; const MINUTE = 1000 * 60; const ROOT_DIR = resolve(__dirname, '../../../../'); const FIXTURE_DIR = resolve(__dirname, '__fixtures__'); const TARGET_DIR = resolve(FIXTURE_DIR, 'target'); -const XML_PATH = resolve(TARGET_DIR, 'junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}Jest Tests.xml`); +const XML_PATH = makeJunitReportPath(FIXTURE_DIR, 'Jest Tests'); afterAll(async () => { await del(TARGET_DIR); diff --git a/src/dev/jest/junit_reporter.js b/src/dev/jest/junit_reporter.js index 7f51326ee46bb8..0f8003f4ed6a1c 100644 --- a/src/dev/jest/junit_reporter.js +++ b/src/dev/jest/junit_reporter.js @@ -22,7 +22,7 @@ import { writeFileSync, mkdirSync } from 'fs'; import xmlBuilder from 'xmlbuilder'; -import { escapeCdata } from '@kbn/test'; +import { escapeCdata, makeJunitReportPath } from '@kbn/test'; const ROOT_DIR = dirname(require.resolve('../../../package.json')); @@ -102,13 +102,7 @@ export default class JestJUnitReporter { }); }); - const reportPath = resolve( - rootDirectory, - 'target/junit', - process.env.JOB || '.', - `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}${reportName}.xml` - ); - + const reportPath = makeJunitReportPath(rootDirectory, reportName); const reportXML = root.end(); mkdirSync(dirname(reportPath), { recursive: true }); writeFileSync(reportPath, reportXML, 'utf8'); diff --git a/src/dev/register_git_hook/register_git_hook.js b/src/dev/register_git_hook/register_git_hook.js index a61922078e6876..31136cab0adae9 100644 --- a/src/dev/register_git_hook/register_git_hook.js +++ b/src/dev/register_git_hook/register_git_hook.js @@ -58,6 +58,15 @@ function getKbnPrecommitGitHookScript(rootPath, nodeHome, platform) { set -euo pipefail + # Make it possible to terminate pre commit hook + # using ctrl-c so nothing else would happen or be + # sent to the output. + # + # The correct exit code on that situation + # according the linux documentation project is 130 + # https://www.tldp.org/LDP/abs/html/exitcodes.html + trap "exit 130" SIGINT + has_node() { command -v node >/dev/null 2>&1 } diff --git a/src/dev/renovate/config.ts b/src/dev/renovate/config.ts index 6acbbaa4d5255a..61feb4e47f0220 100644 --- a/src/dev/renovate/config.ts +++ b/src/dev/renovate/config.ts @@ -123,6 +123,11 @@ export const RENOVATE_CONFIG = { */ rebaseStalePrs: false, + /** + * Disable automatic rebase on conflicts with the base branch + */ + rebaseConflictedPrs: false, + /** * Disable semantic commit formating */ diff --git a/src/dev/renovate/package_globs.ts b/src/dev/renovate/package_globs.ts index dd98f929be8ede..825e6ffed0ec6b 100644 --- a/src/dev/renovate/package_globs.ts +++ b/src/dev/renovate/package_globs.ts @@ -28,6 +28,7 @@ export const PACKAGE_GLOBS = [ 'x-pack/package.json', 'x-pack/legacy/plugins/*/package.json', 'packages/*/package.json', + 'examples/*/package.json', 'test/plugin_functional/plugins/*/package.json', 'test/interpreter_functional/plugins/*/package.json', ]; diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 2f8894f77ebc1d..602657ad738368 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -40,6 +40,9 @@ export const PROJECTS = [ ...glob .sync('packages/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), + ...glob + .sync('examples/*/tsconfig.json', { cwd: REPO_ROOT }) + .map(path => new Project(resolve(REPO_ROOT, path))), ...glob .sync('test/plugin_functional/plugins/*/tsconfig.json', { cwd: REPO_ROOT }) .map(path => new Project(resolve(REPO_ROOT, path))), diff --git a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx index 442ed330e9b7a4..40b9cc4640eef1 100644 --- a/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/legacy/core_plugins/console/public/np_ready/application/containers/editor/legacy/console_editor/editor.tsx @@ -20,6 +20,11 @@ import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; import { EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { debounce } from 'lodash'; + +// Node v5 querystring for browser. +// @ts-ignore +import * as qs from 'querystring-browser'; import { EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useServicesContext, useEditorReadContext } from '../../../../contexts'; @@ -80,17 +85,64 @@ function EditorUI() { useEffect(() => { editorInstanceRef.current = senseEditor.create(editorRef.current!); + const editor = editorInstanceRef.current; + + const readQueryParams = () => { + const [, queryString] = (window.location.hash || '').split('?'); + return qs.parse(queryString || ''); + }; + + const loadBufferFromRemote = (url: string) => { + if (/^https?:\/\//.test(url)) { + const loadFrom: Record = { + url, + // Having dataType here is required as it doesn't allow jQuery to `eval` content + // coming from the external source thereby preventing XSS attack. + dataType: 'text', + kbnXsrfToken: false, + }; + + if (/https?:\/\/api\.github\.com/.test(url)) { + loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' }; + } - const { content: text } = history.getSavedEditorState() || { - content: DEFAULT_INPUT_VALUE, + // Fire and forget. + $.ajax(loadFrom).done(async data => { + const coreEditor = editor.getCoreEditor(); + await editor.update(data, true); + editor.moveToNextRequestEdge(false); + coreEditor.clearSelection(); + editor.highlightCurrentRequestsAndUpdateActionBar(); + coreEditor.getContainer().focus(); + }); + } }; - editorInstanceRef.current.update(text); + + // Support for loading a console snippet from a remote source, like support docs. + const onHashChange = debounce(() => { + const { load_from: url } = readQueryParams(); + if (!url) { + return; + } + loadBufferFromRemote(url); + }, 200); + window.addEventListener('hashchange', onHashChange); + + const initialQueryParams = readQueryParams(); + if (initialQueryParams.load_from) { + loadBufferFromRemote(initialQueryParams.load_from); + } else { + const { content: text } = history.getSavedEditorState() || { + content: DEFAULT_INPUT_VALUE, + }; + editor.update(text); + } function setupAutosave() { let timer: number; const saveDelay = 500; - editorInstanceRef.current!.getCoreEditor().on('change', () => { + editor.getCoreEditor().on('change', () => { if (timer) { clearTimeout(timer); } @@ -100,35 +152,34 @@ function EditorUI() { function saveCurrentState() { try { - const content = editorInstanceRef.current!.getCoreEditor().getValue(); + const content = editor.getCoreEditor().getValue(); history.updateCurrentState(content); } catch (e) { // Ignoring saving error } } - setInputEditor(editorInstanceRef.current); + setInputEditor(editor); setTextArea(editorRef.current!.querySelector('textarea')); mappings.retrieveAutoCompleteInfo(); - const unsubscribeResizer = subscribeResizeChecker( - editorRef.current!, - editorInstanceRef.current.getCoreEditor() - ); + const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor.getCoreEditor()); setupAutosave(); return () => { unsubscribeResizer(); mappings.clearSubscriptions(); + window.removeEventListener('hashchange', onHashChange); }; }, [history, setInputEditor]); useEffect(() => { - applyCurrentSettings(editorInstanceRef.current!.getCoreEditor(), settings); + const { current: editor } = editorInstanceRef; + applyCurrentSettings(editor!.getCoreEditor(), settings); // Preserve legacy focus behavior after settings have updated. - editorInstanceRef - .current!.getCoreEditor() + editor! + .getCoreEditor() .getContainer() .focus(); }, [settings]); @@ -160,6 +211,9 @@ function EditorUI() { + +

diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx index 69bdcf59bb2274..653e7d4215eefe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/dashboard_empty_screen.test.tsx @@ -47,4 +47,15 @@ describe('DashboardEmptyScreen', () => { const paragraph = findTestSubject(component, 'linkToVisualizeParagraph'); expect(paragraph.length).toBe(0); }); + + test('when specified, prop onVisualizeClick is called correctly', () => { + const onVisualizeClick = jest.fn(); + const component = mountComponent({ + ...defaultProps, + ...{ showLinkToVisualize: true, onVisualizeClick }, + }); + const button = findTestSubject(component, 'addVisualizationButton'); + button.simulate('click'); + expect(onVisualizeClick).toHaveBeenCalled(); + }); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts index 01468eadffb841..bf7135098ea74b 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/__tests__/get_saved_dashboard_mock.ts @@ -41,5 +41,5 @@ export function getSavedDashboardMock( getQuery: () => ({ query: '', language: 'kuery' }), getFilters: () => [], ...config, - }; + } as SavedObjectDashboard; } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx index fd49b26e0d948c..24b64a88998f4e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app_controller.tsx @@ -21,7 +21,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import React from 'react'; import angular from 'angular'; -import { uniq, noop } from 'lodash'; +import { uniq } from 'lodash'; import { Subscription } from 'rxjs'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; @@ -35,7 +35,7 @@ import { State, AppStateClass as TAppStateClass, KbnUrl, - SaveOptions, + SavedObjectSaveOpts, unhashUrl, } from './legacy_imports'; import { FilterStateManager, IndexPattern } from '../../../data/public'; @@ -53,6 +53,7 @@ import { ErrorEmbeddable, ViewMode, openAddPanelFlyout, + EmbeddableFactoryNotFoundError, } from '../../../embeddable_api/public/np_ready/public'; import { DashboardAppState, NavAction, ConfirmModalFn, SavedDashboardPanel } from './types'; @@ -145,16 +146,20 @@ export class DashboardAppController { } $scope.showSaveQuery = dashboardCapabilities.saveQuery as boolean; - $scope.getShouldShowEditHelp = () => + const getShouldShowEditHelp = () => !dashboardStateManager.getPanels().length && dashboardStateManager.getIsEditMode() && !dashboardConfig.getHideWriteControls(); - $scope.getShouldShowViewHelp = () => + const getShouldShowViewHelp = () => !dashboardStateManager.getPanels().length && dashboardStateManager.getIsViewMode() && !dashboardConfig.getHideWriteControls(); + const addVisualization = () => { + navActions[TopNavIds.VISUALIZE](); + }; + const updateIndexPatterns = (container?: DashboardContainer) => { if (!container || isErrorEmbeddable(container)) { return; @@ -189,7 +194,7 @@ export class DashboardAppController { showLinkToVisualize: shouldShowEditHelp, }; if (shouldShowEditHelp) { - emptyScreenProps.onVisualizeClick = noop; + emptyScreenProps.onVisualizeClick = addVisualization; } return emptyScreenProps; }; @@ -205,8 +210,8 @@ export class DashboardAppController { if (dashboardContainer && !isErrorEmbeddable(dashboardContainer)) { expandedPanelId = dashboardContainer.getInput().expandedPanelId; } - const shouldShowEditHelp = $scope.getShouldShowEditHelp(); - const shouldShowViewHelp = $scope.getShouldShowViewHelp(); + const shouldShowEditHelp = getShouldShowEditHelp(); + const shouldShowViewHelp = getShouldShowViewHelp(); return { id: dashboardStateManager.savedDashboard.id || '', filters: queryFilter.getFilters(), @@ -261,8 +266,8 @@ export class DashboardAppController { dashboardContainer = container; dashboardContainer.renderEmpty = () => { - const shouldShowEditHelp = $scope.getShouldShowEditHelp(); - const shouldShowViewHelp = $scope.getShouldShowViewHelp(); + const shouldShowEditHelp = getShouldShowEditHelp(); + const shouldShowViewHelp = getShouldShowViewHelp(); const isEmptyState = shouldShowEditHelp || shouldShowViewHelp; return isEmptyState ? ( @@ -603,7 +608,7 @@ export class DashboardAppController { * @return {Promise} * @resolved {String} - The id of the doc */ - function save(saveOptions: SaveOptions): Promise { + function save(saveOptions: SavedObjectSaveOpts): Promise { return saveDashboard(angular.toJson, timefilter, dashboardStateManager, saveOptions) .then(function(id) { if (id) { @@ -759,7 +764,17 @@ export class DashboardAppController { } }; - navActions[TopNavIds.VISUALIZE] = async () => {}; + navActions[TopNavIds.VISUALIZE] = async () => { + const type = 'visualization'; + const factory = embeddables.getEmbeddableFactory(type); + if (!factory) { + throw new EmbeddableFactoryNotFoundError(type); + } + const explicitInput = await factory.getExplicitInput(); + if (dashboardContainer) { + await dashboardContainer.addNewEmbeddable(type, explicitInput); + } + }; navActions[TopNavIds.OPTIONS] = anchorElement => { showOptionsPopover({ diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx index 234228ba4166a9..2fc78d64d0a0cd 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; +import { I18nProvider } from '@kbn/i18n/react'; import { EuiIcon, EuiLink, @@ -26,6 +26,7 @@ import { EuiPageBody, EuiPage, EuiText, + EuiButton, } from '@elastic/eui'; import * as constants from './dashboard_empty_screen_constants'; @@ -38,23 +39,20 @@ export interface DashboardEmptyScreenProps { export function DashboardEmptyScreen({ showLinkToVisualize, onLinkClick, + onVisualizeClick, }: DashboardEmptyScreenProps) { const linkToVisualizeParagraph = ( - -

- - {constants.visualizeAppLinkTest} - - ), - }} - /> -

-
+

+ + {constants.createNewVisualizationButton} + +

); const paragraph = ( description1: string, @@ -96,7 +94,7 @@ export function DashboardEmptyScreen({ ); return ( - + diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx index 0f510375aaf597..03004f6270fef9 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_empty_screen_constants.tsx @@ -76,3 +76,9 @@ export const visualizeAppLinkTest: string = i18n.translate( defaultMessage: 'visit the Visualize app', } ); +export const createNewVisualizationButton: string = i18n.translate( + 'kbn.dashboard.createNewVisualizationButton', + { + defaultMessage: 'Create new', + } +); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index af0a833399a523..adae063a1470bb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -30,7 +30,7 @@ export const legacyChrome = chrome; export { State } from 'ui/state_management/state'; export { AppState } from 'ui/state_management/app_state'; export { AppStateClass } from 'ui/state_management/app_state'; -export { SaveOptions } from 'ui/saved_objects/saved_object'; +export { SavedObjectSaveOpts } from 'ui/saved_objects/types'; export { npSetup, npStart } from 'ui/new_platform'; export { SavedObjectRegistryProvider } from 'ui/saved_objects'; export { IPrivate } from 'ui/private'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts index e0d82373d3ad9d..e322677433ce6f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/lib/save_dashboard.ts @@ -18,7 +18,7 @@ */ import { TimefilterContract } from 'src/plugins/data/public'; -import { SaveOptions } from '../legacy_imports'; +import { SavedObjectSaveOpts } from '../legacy_imports'; import { updateSavedDashboard } from './update_saved_dashboard'; import { DashboardStateManager } from '../dashboard_state_manager'; @@ -34,7 +34,7 @@ export function saveDashboard( toJson: (obj: any) => string, timeFilter: TimefilterContract, dashboardStateManager: DashboardStateManager, - saveOptions: SaveOptions + saveOptions: SavedObjectSaveOpts ): Promise { dashboardStateManager.saveState(); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts index 5300811a6dab93..1ab5738cf47525 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +// This file should be moved to dashboard/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsMigrationLogger } from 'src/core/server'; import { inspect } from 'util'; import { DashboardDoc730ToLatest, DashboardDoc700To720 } from './types'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts index 4c417ed2954d3f..20544fa97fdb09 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboard.d.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { SearchSourceContract } from '../../../../../ui/public/courier'; import { esFilters, Query, RefreshInterval } from '../../../../../../plugins/data/public'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js index 9b7d036590f1da..6b561c18a5d422 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/saved_dashboard/saved_dashboards.js @@ -22,6 +22,7 @@ import './saved_dashboard'; import { uiModules } from 'ui/modules'; import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; +import { npStart } from '../../../../../ui/public/new_platform'; const module = uiModules.get('app/dashboard'); @@ -35,7 +36,7 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedDashboards', function (Private, SavedDashboard, kbnUrl, chrome) { +module.service('savedDashboards', function (Private, SavedDashboard) { const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedDashboard, kbnUrl, chrome, savedObjectClient); + return new SavedObjectLoader(SavedDashboard, savedObjectClient, npStart.core.chrome); }); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 92df04c536e438..5c6ba86455588d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -32,7 +32,7 @@ describe('discoverField', function () { let $scope; let indexPattern; let $elem; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.inject(function (Private, $rootScope, $compile) { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index 34c6483349af60..a707402ff92b02 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -70,7 +70,7 @@ describe('discover field chooser directives', function () { on-remove-field="removeField" > `); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover', ($provide) => { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js index 645ca32924ede8..aa3c260eed52de 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_add_filter.js @@ -27,7 +27,7 @@ import { npStart } from 'ui/new_platform'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); beforeEach(ngMock.module(function createServiceStubs($provide) { $provide.value('indexPatterns', createIndexPatternsStub()); diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js index a8bef6fe75c79d..e1821eada79edb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_predecessor_count.js @@ -26,7 +26,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setPredecessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js index a43a8a11a7bf82..348ea2c55790aa 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_query_parameters.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setQueryParameters', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js index 4bbd462aaa4b08..c03158722347b7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/context/query_parameters/__tests__/action_set_successor_count.js @@ -27,7 +27,7 @@ import { getQueryParameterActions } from '../actions'; describe('context app', function () { beforeEach(() => pluginInstance.initializeInnerAngular()); - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(ngMock.module('app/discover')); describe('action setSuccessorCount', function () { diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js index 666261610fa7cf..c8e8e216d0deac 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/__tests__/lib/rows_headers.js @@ -37,7 +37,7 @@ describe('Doc Table', function () { let fakeRowVals; let stubFieldFormatConverter; - beforeEach(() => pluginInstance.initializeServices(true)); + beforeEach(() => pluginInstance.initializeServices()); beforeEach(() => pluginInstance.initializeInnerAngular()); beforeEach(ngMock.module('app/discover')); beforeEach( diff --git a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_row/cell.html b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_row/cell.html index 77702ed6064740..0704016a52bbdd 100644 --- a/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_row/cell.html +++ b/src/legacy/core_plugins/kibana/public/discover/angular/doc_table/components/table_row/cell.html @@ -19,9 +19,9 @@ tooltip-append-to-body="1" data-test-subj="docTableCellFilter" tooltip="{{ ::'kbn.docTable.tableRow.filterForValueButtonTooltip' | i18n: {defaultMessage: 'Filter for value'} }}" + tooltip-placement="bottom" aria-label="{{ ::'kbn.docTable.tableRow.filterForValueButtonAriaLabel' | i18n: {defaultMessage: 'Filter for value'} }}" > - <% } %> diff --git a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts index b72bd27a31cf97..14ea4e99b0de46 100644 --- a/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/helpers/build_services.ts @@ -25,14 +25,12 @@ import { IUiSettingsClient, } from 'kibana/public'; import * as docViewsRegistry from 'ui/registry/doc_views'; -import chromeLegacy from 'ui/chrome'; -import { IPrivate } from 'ui/private'; import { FilterManager, TimefilterContract, IndexPatternsContract } from 'src/plugins/data/public'; // @ts-ignore import { createSavedSearchesService } from '../saved_searches/saved_searches'; // @ts-ignore -import { createSavedSearchFactory } from '../saved_searches/_saved_search'; import { DiscoverStartPlugins } from '../plugin'; +import { DataStart } from '../../../../data/public'; import { EuiUtilsStart } from '../../../../../../plugins/eui_utils/public'; import { SavedSearch } from '../types'; import { SharePluginStart } from '../../../../../../plugins/share/public'; @@ -42,6 +40,7 @@ export interface DiscoverServices { capabilities: Capabilities; chrome: ChromeStart; core: CoreStart; + data: DataStart; docLinks: DocLinksStart; docViewsRegistry: docViewsRegistry.DocViewsRegistry; eui_utils: EuiUtilsStart; @@ -52,35 +51,19 @@ export interface DiscoverServices { share: SharePluginStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; - // legacy getSavedSearchById: (id: string) => Promise; getSavedSearchUrlById: (id: string) => Promise; uiSettings: IUiSettingsClient; } - -export async function buildGlobalAngularServices() { - const injector = await chromeLegacy.dangerouslyGetActiveInjector(); - const Private = injector.get('Private'); - const kbnUrl = injector.get('kbnUrl'); - const SavedSearchFactory = createSavedSearchFactory(Private); - const service = createSavedSearchesService(Private, SavedSearchFactory, kbnUrl, chromeLegacy); - return { - getSavedSearchById: async (id: string) => service.get(id), - getSavedSearchUrlById: async (id: string) => service.urlFor(id), +export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins) { + const services = { + savedObjectsClient: core.savedObjects.client, + indexPatterns: plugins.data.indexPatterns, + chrome: core.chrome, + overlays: core.overlays, }; -} - -export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugins, test: false) { - const globalAngularServices = !test - ? await buildGlobalAngularServices() - : { - getSavedSearchById: async (id: string) => void id, - getSavedSearchUrlById: async (id: string) => void id, - State: null, - }; - + const savedObjectService = createSavedSearchesService(services); return { - ...globalAngularServices, addBasePath: core.http.basePath.prepend, capabilities: core.application.capabilities, chrome: core.chrome, @@ -90,6 +73,8 @@ export async function buildServices(core: CoreStart, plugins: DiscoverStartPlugi docViewsRegistry, eui_utils: plugins.eui_utils, filterManager: plugins.data.query.filterManager, + getSavedSearchById: async (id: string) => savedObjectService.get(id), + getSavedSearchUrlById: async (id: string) => savedObjectService.urlFor(id), indexPatterns: plugins.data.indexPatterns, inspector: plugins.inspector, // @ts-ignore diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts index 7f8ca4e96c5aca..35c8da9e3f2655 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -23,10 +23,8 @@ import { DiscoverPlugin, DiscoverSetup, DiscoverStart } from './plugin'; import { start as navigation } from '../../../navigation/public/legacy'; // Core will be looking for this when loading our plugin in the new platform -export const plugin: PluginInitializer = ( - initializerContext: PluginInitializerContext -) => { - return new DiscoverPlugin(initializerContext); +export const plugin: PluginInitializer = () => { + return new DiscoverPlugin(); }; // Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 6679e8a3b88263..cb84463c184386 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; +import { AppMountParameters, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import angular from 'angular'; import { IUiActionsStart } from 'src/plugins/ui_actions/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; @@ -69,14 +69,13 @@ export class DiscoverPlugin implements Plugin { */ public initializeInnerAngular?: () => void; public initializeServices?: () => void; - constructor(initializerContext: PluginInitializerContext) {} setup(core: CoreSetup, plugins: DiscoverSetupPlugins): DiscoverSetup { plugins.kibana_legacy.registerLegacyApp({ id: 'discover', title: 'Discover', order: -1004, euiIconType: 'discoverApp', - mount: async (context, params) => { + mount: async (params: AppMountParameters) => { if (!this.initializeServices) { throw Error('Discover plugin method initializeServices is undefined'); } @@ -106,11 +105,11 @@ export class DiscoverPlugin implements Plugin { this.innerAngularInitialized = true; }; - this.initializeServices = async (test = false) => { + this.initializeServices = async () => { if (this.servicesInitialized) { return; } - const services = await buildServices(core, plugins, test); + const services = await buildServices(core, plugins); setServices(services); this.servicesInitialized = true; }; diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js deleted file mode 100644 index db2b2b5b22af7a..00000000000000 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.js +++ /dev/null @@ -1,72 +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 { createLegacyClass } from 'ui/utils/legacy_class'; -import { SavedObjectProvider } from 'ui/saved_objects/saved_object'; - -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('discover/saved_searches', []); - -export function createSavedSearchFactory(Private) { - const SavedObject = Private(SavedObjectProvider); - createLegacyClass(SavedSearch).inherits(SavedObject); - function SavedSearch(id) { - SavedObject.call(this, { - type: SavedSearch.type, - mapping: SavedSearch.mapping, - searchSource: SavedSearch.searchSource, - id: id, - defaults: { - title: '', - description: '', - columns: [], - hits: 0, - sort: [], - version: 1, - }, - }); - - this.showInRecentlyAccessed = true; - } - - SavedSearch.type = 'search'; - - SavedSearch.mapping = { - title: 'text', - description: 'text', - hits: 'integer', - columns: 'keyword', - sort: 'keyword', - version: 'integer', - }; - - // Order these fields to the top, the rest are alphabetical - SavedSearch.fieldOrder = ['title', 'description']; - - SavedSearch.searchSource = true; - - SavedSearch.prototype.getFullPath = function () { - return `/app/kibana#/discover/${this.id}`; - }; - - return SavedSearch; -} - -module.factory('SavedSearch', createSavedSearchFactory); diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts new file mode 100644 index 00000000000000..113d13287bd122 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/_saved_search.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; + +export function createSavedSearchClass(services: SavedObjectKibanaServices) { + const SavedObjectClass = createSavedObjectClass(services); + + class SavedSearch extends SavedObjectClass { + public static type: string = 'search'; + public static mapping = { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = true; + + public id: string; + public showInRecentlyAccessed: boolean; + + constructor(id: string) { + super({ + id, + type: 'search', + mapping: { + title: 'text', + description: 'text', + hits: 'integer', + columns: 'keyword', + sort: 'keyword', + version: 'integer', + }, + searchSource: true, + defaults: { + title: '', + description: '', + columns: [], + hits: 0, + sort: [], + version: 1, + }, + }); + this.showInRecentlyAccessed = true; + this.id = id; + this.getFullPath = () => `/app/kibana#/discover/${String(id)}`; + } + } + + return SavedSearch; +} diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/index.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/index.ts diff --git a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts similarity index 60% rename from src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js rename to src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts index 7ebcba903cc7f3..46fdd3a7baedc2 100644 --- a/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.js +++ b/src/legacy/core_plugins/kibana/public/discover/saved_searches/saved_searches.ts @@ -16,12 +16,14 @@ * specific language governing permissions and limitations * under the License. */ - -import './_saved_search'; +import { npStart } from 'ui/new_platform'; +// @ts-ignore import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { SavedObjectKibanaServices } from 'ui/saved_objects/types'; +// @ts-ignore import { savedObjectManagementRegistry } from '../../management/saved_object_registry'; - +import { createSavedSearchClass } from './_saved_search'; // Register this service with the saved object registry so it can be // edited by the object editor. @@ -30,9 +32,13 @@ savedObjectManagementRegistry.register({ title: 'searches', }); -export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSearchLoader = new SavedObjectLoader(SavedSearch, kbnUrl, chrome, savedObjectClient); +export function createSavedSearchesService(services: SavedObjectKibanaServices) { + const SavedSearchClass = createSavedSearchClass(services); + const savedSearchLoader = new SavedObjectLoader( + SavedSearchClass, + services.savedObjectsClient, + services.chrome + ); // Customize loader properties since adding an 's' on type doesn't work for type 'search' . savedSearchLoader.loaderProperties = { name: 'searches', @@ -40,11 +46,18 @@ export function createSavedSearchesService(Private, SavedSearch, kbnUrl, chrome) nouns: 'saved searches', }; - savedSearchLoader.urlFor = (id) => { - return kbnUrl.eval('#/discover/{{id}}', { id: id }); - }; + savedSearchLoader.urlFor = (id: string) => `#/discover/${encodeURIComponent(id)}`; return savedSearchLoader; } +// this is needed for saved object management const module = uiModules.get('discover/saved_searches'); -module.service('savedSearches', createSavedSearchesService); +module.service('savedSearches', () => { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedSearchesService(services); +}); diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png new file mode 100644 index 00000000000000..22136049b494ad Binary files /dev/null and b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/azure_metrics/screenshot.png differ diff --git a/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg new file mode 100644 index 00000000000000..f8df12ba05c508 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/home/tutorial_resources/logos/azure.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index e5bfd88ea76378..cd3e0d2fd9f89c 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { App, AppUnmount } from 'kibana/public'; +import { App, AppUnmount, AppMountDeprecated } from 'kibana/public'; import { UIRoutes } from 'ui/routes'; import { ILocationService, IScope } from 'angular'; import { npStart } from 'ui/new_platform'; @@ -68,7 +68,10 @@ export class LocalApplicationService { isUnmounted = true; }); (async () => { - unmountHandler = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + const params = { element, appBasePath: '' }; + unmountHandler = isAppMountDeprecated(app.mount) + ? await app.mount({ core: npStart.core }, params) + : await app.mount(params); // immediately unmount app if scope got destroyed in the meantime if (isUnmounted) { unmountHandler(); @@ -90,3 +93,8 @@ export class LocalApplicationService { } export const localApplicationService = new LocalApplicationService(); + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js index 833ca8467292e3..fd92987614283e 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/index.js @@ -20,7 +20,7 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects'; import uiRoutes from 'ui/routes'; import angularTemplate from './angular_template.html'; -import 'ui/index_patterns'; +import { npStart } from 'ui/new_platform'; import { setup as managementSetup } from '../../../../../../management/public/legacy'; import { getCreateBreadcrumbs } from '../breadcrumbs'; @@ -41,7 +41,7 @@ uiRoutes.when('/management/kibana/index_pattern', { const services = { config: $injector.get('config'), es: $injector.get('es'), - indexPatterns: $injector.get('indexPatterns'), + indexPatterns: npStart.plugins.data.indexPatterns, $http: $injector.get('$http'), savedObjectsClient: Private(SavedObjectsClientProvider), indexPatternCreationType, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js index 5f0994abc11e48..f1c2d6598b1348 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/create_edit_field/create_edit_field.js @@ -23,6 +23,7 @@ import { docTitle } from 'ui/doc_title'; import { KbnUrlProvider } from 'ui/url'; import uiRoutes from 'ui/routes'; import { toastNotifications } from 'ui/notify'; +import { npStart } from 'ui/new_platform'; import template from './create_edit_field.html'; import { getEditFieldBreadcrumbs, getCreateFieldBreadcrumbs } from '../../breadcrumbs'; @@ -96,7 +97,8 @@ uiRoutes }); }, resolve: { - indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) { + indexPattern: function ($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)) .catch(redirectWhenMissing('/management/kibana/index_patterns')); } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js index 150fae6e87ddec..c5ffa0826f10e7 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js @@ -36,6 +36,7 @@ import { IndexedFieldsTable } from './indexed_fields_table'; import { ScriptedFieldsTable } from './scripted_fields_table'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; import { getEditBreadcrumbs } from '../breadcrumbs'; @@ -168,7 +169,8 @@ uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { template, k7Breadcrumbs: getEditBreadcrumbs, resolve: { - indexPattern: function ($route, Promise, redirectWhenMissing, indexPatterns) { + indexPattern: function ($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( redirectWhenMissing('/management/kibana/index_patterns') ); @@ -179,7 +181,7 @@ uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { uiModules .get('apps/management') .controller('managementIndexPatternsEdit', function ( - $scope, $location, $route, Promise, config, indexPatterns, Private, AppState, confirmModal) { + $scope, $location, $route, Promise, config, Private, AppState, confirmModal) { const $state = $scope.state = new AppState(); $scope.fieldWildcardMatcher = (...args) => fieldWildcardMatcher(...args, config.get('metaFields')); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index 32cdce1bb4ac4c..bd3aa20d097c95 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -36,7 +36,7 @@ const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; function updateObjectsTable($scope, $injector) { const Private = $injector.get('Private'); - const indexPatterns = $injector.get('indexPatterns'); + const indexPatterns = npStart.plugins.data.indexPatterns; const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts index 029cb620b52c7f..2444d18133af4d 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/lib/process_import_response.ts @@ -24,7 +24,7 @@ import { SavedObjectsImportMissingReferencesError, SavedObjectsImportUnknownError, SavedObjectsImportError, -} from 'src/core/server'; +} from 'src/core/public'; export interface ProcessedImportResponse { failedImports: Array<{ diff --git a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts index 7ab60f8867c38d..8e414984a0c08d 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/embeddable/visualize_embeddable.ts @@ -22,7 +22,7 @@ import { PersistedState } from 'ui/persisted_state'; import { Subscription } from 'rxjs'; import * as Rx from 'rxjs'; import { buildPipeline } from 'ui/visualize/loader/pipeline_helpers'; -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { Vis } from 'ui/vis'; import { queryGeohashBounds } from 'ui/visualize/loader/utils'; import { getTableAggs } from 'ui/visualize/loader/pipeline_helpers/utilities'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js index ae4b4d1c779df1..a30973ab6a4616 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/_saved_vis.js @@ -38,7 +38,7 @@ import { uiModules .get('app/visualize') - .factory('SavedVis', function (Promise, savedSearches, Private) { + .factory('SavedVis', function (savedSearches, Private) { const SavedObject = Private(SavedObjectProvider); createLegacyClass(SavedVis).inherits(SavedObject); function SavedVis(opts) { @@ -95,18 +95,15 @@ uiModules return `/app/kibana#${VisualizeConstants.EDIT_PATH}/${this.id}`; }; - SavedVis.prototype._afterEsResp = function () { + SavedVis.prototype._afterEsResp = async function () { const self = this; - return self._getLinkedSavedSearch() - .then(function () { - self.searchSource.setField('size', 0); - - return self.vis ? self._updateVis() : self._createVis(); - }); + await self._getLinkedSavedSearch(); + self.searchSource.setField('size', 0); + return self.vis ? self._updateVis() : self._createVis(); }; - SavedVis.prototype._getLinkedSavedSearch = Promise.method(function () { + SavedVis.prototype._getLinkedSavedSearch = async function () { const self = this; const linkedSearch = !!self.savedSearchId; const current = self.savedSearch; @@ -122,13 +119,10 @@ uiModules } if (linkedSearch) { - return savedSearches.get(self.savedSearchId) - .then(function (savedSearch) { - self.savedSearch = savedSearch; - self.searchSource.setParent(self.savedSearch.searchSource); - }); + self.savedSearch = await savedSearches.get(self.savedSearchId); + self.searchSource.setParent(self.savedSearch.searchSource); } - }); + }; SavedVis.prototype._createVis = function () { const self = this; diff --git a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js index c391eb872e29d7..dd12b7e38798fc 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js +++ b/src/legacy/core_plugins/kibana/public/visualize/saved_visualizations/saved_visualizations.js @@ -24,6 +24,7 @@ import { savedObjectManagementRegistry } from '../../management/saved_object_reg import { start as visualizations } from '../../../../visualizations/public/np_ready/public/legacy'; import { createVisualizeEditUrl } from '../visualize_constants'; import { findListItems } from './find_list_items'; +import { npStart } from '../../../../../ui/public/new_platform'; const app = uiModules.get('app/visualize'); @@ -34,14 +35,13 @@ savedObjectManagementRegistry.register({ title: 'visualizations', }); -app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) { +app.service('savedVisualizations', function (SavedVis, Private) { const visTypes = visualizations.types; const savedObjectClient = Private(SavedObjectsClientProvider); const saveVisualizationLoader = new SavedObjectLoader( SavedVis, - kbnUrl, - chrome, - savedObjectClient + savedObjectClient, + npStart.core.chrome ); saveVisualizationLoader.mapHitSource = function (source, id) { @@ -73,7 +73,7 @@ app.service('savedVisualizations', function (SavedVis, Private, kbnUrl, chrome) }; saveVisualizationLoader.urlFor = function (id) { - return kbnUrl.eval('#/visualize/edit/{{id}}', { id: id }); + return `#/visualize/edit/${encodeURIComponent(id)}`; }; // This behaves similarly to find, except it returns visualizations that are diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap index 04b7cddc752891..ca6b872c73f8f5 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -242,13 +242,70 @@ exports[`NewVisModal filter for visualization types should render as expected 1` aria-live="polite" class="euiScreenReaderOnly" > - 1 type found + 2 types found + + + + + Vis with alias Url + + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + role="menuitem" + > + + ({ State: () => null, AppState: () => null, @@ -32,6 +33,7 @@ import { NewVisModal } from './new_vis_modal'; import { SavedObjectsStart } from 'kibana/public'; describe('NewVisModal', () => { + const { location } = window; const defaultVisTypeParams = { hidden: false, visualization: class Controller { @@ -51,6 +53,12 @@ describe('NewVisModal', () => { stage: 'production', ...defaultVisTypeParams, }, + { + name: 'visWithAliasUrl', + title: 'Vis with alias Url', + stage: 'production', + aliasUrl: '/aliasUrl', + }, ]; const visTypes: TypesStart = { get: (id: string) => { @@ -69,6 +77,10 @@ describe('NewVisModal', () => { jest.clearAllMocks(); }); + afterAll(() => { + window.location = location; + }); + it('should render as expected', () => { const wrapper = mountWithIntl( { visButton.simulate('click'); expect(window.location.assign).toBeCalledWith('#/visualize/create?type=vis&foo=true&bar=42'); }); + + it('closes if visualization with aliasUrl and addToDashboard in editorParams', () => { + const onClose = jest.fn(); + window.location.assign = jest.fn(); + const wrapper = mountWithIntl( + + ); + const visButton = wrapper.find('button[data-test-subj="visType-visWithAliasUrl"]'); + visButton.simulate('click'); + expect(window.location.assign).toBeCalledWith('testbasepath/aliasUrl'); + expect(onClose).toHaveBeenCalled(); + }); }); describe('filter for visualization types', () => { diff --git a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx index 0402265610fb12..e84797302589d3 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx +++ b/src/legacy/core_plugins/kibana/public/visualize/wizard/new_vis_modal.tsx @@ -132,8 +132,10 @@ class NewVisModal extends React.Component { diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx index fa0e5fcac74072..0b3b7b5b2a2729 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx @@ -18,11 +18,11 @@ */ import React from 'react'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { TopNavMenuProps, TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; -import { DataStart } from '../../../../core_plugins/data/public'; -export function createTopNav(data: DataStart, extraConfig: TopNavMenuData[]) { +export function createTopNav(data: DataPublicPluginStart, extraConfig: TopNavMenuData[]) { return (props: TopNavMenuProps) => { const config = (props.config || []).concat(extraConfig); diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 9077de89103277..4e2ea44bf76422 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -22,8 +22,6 @@ import { TopNavMenu } from './top_nav_menu'; import { TopNavMenuData } from './top_nav_menu_data'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -jest.mock('ui/new_platform'); - const mockTimeHistory = { add: () => {}, get: () => { diff --git a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 14599e76470c0a..d5f951ea3eeeaa 100644 --- a/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/legacy/core_plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -24,13 +24,13 @@ import { I18nProvider } from '@kbn/i18n/react'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -import { SearchBarProps, DataStart } from '../../../../core_plugins/data/public'; +import { SearchBarProps, DataPublicPluginStart } from '../../../../../plugins/data/public'; export type TopNavMenuProps = Partial & { appName: string; config?: TopNavMenuData[]; showSearchBar?: boolean; - data?: DataStart; + data?: DataPublicPluginStart; }; /* diff --git a/src/legacy/core_plugins/telemetry/common/constants.ts b/src/legacy/core_plugins/telemetry/common/constants.ts index 7b0c62276f2902..7e366676a8565b 100644 --- a/src/legacy/core_plugins/telemetry/common/constants.ts +++ b/src/legacy/core_plugins/telemetry/common/constants.ts @@ -70,3 +70,8 @@ export const TELEMETRY_STATS_TYPE = 'telemetry'; * @type {string} */ export const UI_METRIC_USAGE_TYPE = 'ui_metric'; + +/** + * Link to Advanced Settings. + */ +export const PATH_TO_ADVANCED_SETTINGS = 'kibana#/management/kibana/settings'; diff --git a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap index 9c26909dc68f18..193205cd394e24 100644 --- a/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap +++ b/src/legacy/core_plugins/telemetry/public/components/__snapshots__/opted_in_notice_banner.test.tsx.snap @@ -10,7 +10,7 @@ exports[`OptInDetailsComponent renders as expected 1`] = ` values={ Object { "disableLink": any; @@ -57,7 +59,10 @@ export class OptedInBanner extends React.PureComponent { ), disableLink: ( - + bannerId, getOptInBannerNoticeId: () => optInBannerNoticeId, @@ -116,7 +113,6 @@ export function TelemetryOptInProvider($injector: any, chrome: any, sendOptInSta if (!allowChangingOptInStatus) { return; } - setCanTrackUiMetrics(enabled); const $http = $injector.get('$http'); try { diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js index 81d459f70a5e1d..dc3a5be40dff4c 100644 --- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js +++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_suggestions/arg_value_suggestions.js @@ -18,11 +18,11 @@ */ import _ from 'lodash'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; +import { npStart } from 'ui/new_platform'; -export function ArgValueSuggestionsProvider(Private, indexPatterns) { - - const savedObjectsClient = Private(SavedObjectsClientProvider); +export function ArgValueSuggestionsProvider() { + const { indexPatterns } = npStart.plugins.data; + const { client: savedObjectsClient } = npStart.core.savedObjects; async function getIndexPattern(functionArgs) { const indexPatternArg = functionArgs.find(argument => { diff --git a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js index d303069e74dea5..86cde9e06240a5 100644 --- a/src/legacy/core_plugins/timelion/public/services/saved_sheets.js +++ b/src/legacy/core_plugins/timelion/public/services/saved_sheets.js @@ -21,6 +21,7 @@ import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects' import { savedObjectManagementRegistry } from 'plugins/kibana/management/saved_object_registry'; import { uiModules } from 'ui/modules'; import './_saved_sheet.js'; +import { npStart } from '../../../../ui/public/new_platform'; const module = uiModules.get('app/sheet'); @@ -32,9 +33,9 @@ savedObjectManagementRegistry.register({ }); // This is the only thing that gets injected into controllers -module.service('savedSheets', function (Private, SavedSheet, kbnUrl, chrome) { +module.service('savedSheets', function (Private, SavedSheet, kbnUrl) { const savedObjectClient = Private(SavedObjectsClientProvider); - const savedSheetLoader = new SavedObjectLoader(SavedSheet, kbnUrl, chrome, savedObjectClient); + const savedSheetLoader = new SavedObjectLoader(SavedSheet, savedObjectClient, npStart.core.chrome); savedSheetLoader.urlFor = function (id) { return kbnUrl.eval('#/{{id}}', { id: id }); }; diff --git a/src/legacy/core_plugins/ui_metric/README.md b/src/legacy/core_plugins/ui_metric/README.md deleted file mode 100644 index 90855faff61a69..00000000000000 --- a/src/legacy/core_plugins/ui_metric/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# UI Metric app - -## Purpose - -The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with -various UIs within Kibana. It's useful for gathering _aggregate_ information, e.g. "How many times -has Button X been clicked" or "How many times has Page Y been viewed". - -With some finagling, it's even possible to add more meaning to the info you gather, such as "How many -visualizations were created in less than 5 minutes". - -### What it doesn't do - -The UI Metric app doesn't gather any metadata around a user interaction, e.g. the user's identity, -the name of a dashboard they've viewed, or the timestamp of the interaction. - -## How to use it - -To track a user interaction, import the `createUiStatsReporter` helper function from UI Metric app: - -```js -import { createUiStatsReporter, METRIC_TYPE } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; -const trackMetric = createUiStatsReporter(``); -trackMetric(METRIC_TYPE.CLICK, ``); -trackMetric('click', ``); -``` - -Metric Types: - - `METRIC_TYPE.CLICK` for tracking clicks `trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');` - - `METRIC_TYPE.LOADED` for a component load or page load `trackMetric(METRIC_TYPE.LOADED', 'my_component_loaded');` - - `METRIC_TYPE.COUNT` for a tracking a misc count `trackMetric(METRIC_TYPE.COUNT', 'my_counter', });` - -Call this function whenever you would like to track a user interaction within your app. The function -accepts two arguments, `metricType` and `eventNames`. These should be underscore-delimited strings. -For example, to track the `my_event` metric in the app `my_app` call `trackUiMetric(METRIC_TYPE.*, 'my_event)`. - -That's all you need to do! - -To track multiple metrics within a single request, provide an array of events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`. - -### Disallowed characters - -The colon character (`:`) should not be used in app name or event names. Colons play -a special role in how metrics are stored as saved objects. - -### Tracking timed interactions - -If you want to track how long it takes a user to do something, you'll need to implement the timing -logic yourself. You'll also need to predefine some buckets into which the UI metric can fall. -For example, if you're timing how long it takes to create a visualization, you may decide to -measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes. -To track these interactions, you'd use the timed length of the interaction to determine whether to -use a `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. - -## How it works - -Under the hood, your app and metric type will be stored in a saved object of type `user-metric` and the -ID `ui-metric:my_app:my_metric`. This saved object will have a `count` property which will be incremented -every time the above URI is hit. - -These saved objects are automatically consumed by the stats API and surfaced under the -`ui_metric` namespace. - -```json -{ - "ui_metric": { - "my_app": [ - { - "key": "my_metric", - "value": 3 - } - ] - } -} -``` - -By storing these metrics and their counts as key-value pairs, we can add more metrics without having -to worry about exceeding the 1000-field soft limit in Elasticsearch. \ No newline at end of file diff --git a/src/legacy/core_plugins/ui_metric/index.ts b/src/legacy/core_plugins/ui_metric/index.ts index 964e3497accba7..86d75a9f1818a8 100644 --- a/src/legacy/core_plugins/ui_metric/index.ts +++ b/src/legacy/core_plugins/ui_metric/index.ts @@ -18,10 +18,7 @@ */ import { resolve } from 'path'; -import JoiNamespace from 'joi'; -import { Server } from 'hapi'; import { Legacy } from '../../../../kibana'; -import { registerUiMetricRoute } from './server/routes/api/ui_metric'; // eslint-disable-next-line import/no-default-export export default function(kibana: any) { @@ -29,25 +26,16 @@ export default function(kibana: any) { id: 'ui_metric', require: ['kibana', 'elasticsearch'], publicDir: resolve(__dirname, 'public'), - config(Joi: typeof JoiNamespace) { - return Joi.object({ - enabled: Joi.boolean().default(true), - debug: Joi.boolean().default(Joi.ref('$dev')), - }).default(); - }, uiExports: { - injectDefaultVars(server: Server) { - const config = server.config(); - return { - uiMetricEnabled: config.get('ui_metric.enabled'), - debugUiMetric: config.get('ui_metric.debug'), - }; - }, mappings: require('./mappings.json'), - hacks: ['plugins/ui_metric/hacks/ui_metric_init'], }, init(server: Legacy.Server) { - registerUiMetricRoute(server); + const { getSavedObjectsRepository } = server.savedObjects; + const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); + const internalRepository = getSavedObjectsRepository(callWithInternalUser); + const { usageCollection } = server.newPlatform.setup.plugins; + + usageCollection.registerLegacySavedObjects(internalRepository); }, }); } diff --git a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts b/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts deleted file mode 100644 index 983434f09922b3..00000000000000 --- a/src/legacy/core_plugins/ui_metric/public/hacks/ui_metric_init.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// @ts-ignore -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; -import { kfetch } from 'ui/kfetch'; -import { - createAnalyticsReporter, - setTelemetryReporter, - trackUserAgent, -} from '../services/telemetry_analytics'; -import { isUnauthenticated } from '../../../telemetry/public/services'; - -function telemetryInit($injector: any) { - const uiMetricEnabled = chrome.getInjected('uiMetricEnabled'); - const debug = chrome.getInjected('debugUiMetric'); - if (!uiMetricEnabled || isUnauthenticated()) { - return; - } - const localStorage = $injector.get('localStorage'); - - const uiReporter = createAnalyticsReporter({ localStorage, debug, kfetch }); - setTelemetryReporter(uiReporter); - uiReporter.start(); - trackUserAgent('kibana'); -} - -uiModules.get('kibana').run(telemetryInit); diff --git a/src/legacy/core_plugins/ui_metric/public/index.ts b/src/legacy/core_plugins/ui_metric/public/index.ts index 5c327234b1e7cd..19246b571cb842 100644 --- a/src/legacy/core_plugins/ui_metric/public/index.ts +++ b/src/legacy/core_plugins/ui_metric/public/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { createUiStatsReporter, trackUserAgent } from './services/telemetry_analytics'; +export { createUiStatsReporter } from './services/telemetry_analytics'; export { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; diff --git a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts index ee928b8a1d9ee8..0e517e6ff22449 100644 --- a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts +++ b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts @@ -16,61 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { npSetup } from 'ui/new_platform'; -import { Reporter, UiStatsMetricType } from '@kbn/analytics'; -// @ts-ignore -import { addSystemApiHeader } from 'ui/system_api'; - -let telemetryReporter: Reporter; - -export const setTelemetryReporter = (aTelemetryReporter: Reporter): void => { - telemetryReporter = aTelemetryReporter; -}; - -export const getTelemetryReporter = () => { - return telemetryReporter; -}; - -export const createUiStatsReporter = (appName: string) => ( - type: UiStatsMetricType, - eventNames: string | string[], - count?: number -): void => { - if (telemetryReporter) { - return telemetryReporter.reportUiStats(appName, type, eventNames, count); - } +export const createUiStatsReporter = (appName: string) => { + const { usageCollection } = npSetup.plugins; + return usageCollection.reportUiStats.bind(usageCollection, appName); }; - -export const trackUserAgent = (appName: string) => { - if (telemetryReporter) { - return telemetryReporter.reportUserAgent(appName); - } -}; - -interface AnalyicsReporterConfig { - localStorage: any; - debug: boolean; - kfetch: any; -} - -export function createAnalyticsReporter(config: AnalyicsReporterConfig) { - const { localStorage, debug, kfetch } = config; - - return new Reporter({ - debug, - storage: localStorage, - async http(report) { - const response = await kfetch({ - method: 'POST', - pathname: '/api/telemetry/report', - body: JSON.stringify(report), - headers: addSystemApiHeader({}), - }); - - if (response.status !== 'ok') { - throw Error('Unable to store report.'); - } - return response; - }, - }); -} diff --git a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts b/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts deleted file mode 100644 index e2de23ea806e44..00000000000000 --- a/src/legacy/core_plugins/ui_metric/server/routes/api/ui_metric.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Joi from 'joi'; -import { Report } from '@kbn/analytics'; -import { Server } from 'hapi'; - -export async function storeReport(server: any, report: Report) { - const { getSavedObjectsRepository } = server.savedObjects; - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - const internalRepository = getSavedObjectsRepository(callWithInternalUser); - - const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : []; - const userAgents = report.userAgent ? Object.entries(report.userAgent) : []; - return Promise.all([ - ...userAgents.map(async ([key, metric]) => { - const { userAgent } = metric; - const savedObjectId = `${key}:${userAgent}`; - return await internalRepository.create( - 'ui-metric', - { count: 1 }, - { - id: savedObjectId, - overwrite: true, - } - ); - }), - ...uiStatsMetrics.map(async ([key, metric]) => { - const { appName, eventName } = metric; - const savedObjectId = `${appName}:${eventName}`; - return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); - }), - ]); -} - -export function registerUiMetricRoute(server: Server) { - server.route({ - method: 'POST', - path: '/api/telemetry/report', - options: { - validate: { - payload: Joi.object({ - reportVersion: Joi.number().optional(), - userAgent: Joi.object() - .pattern( - /.*/, - Joi.object({ - key: Joi.string().required(), - type: Joi.string().required(), - appName: Joi.string().required(), - userAgent: Joi.string().required(), - }) - ) - .allow(null) - .optional(), - uiStatsMetrics: Joi.object() - .pattern( - /.*/, - Joi.object({ - key: Joi.string().required(), - type: Joi.string().required(), - appName: Joi.string().required(), - eventName: Joi.string().required(), - stats: Joi.object({ - min: Joi.number(), - sum: Joi.number(), - max: Joi.number(), - avg: Joi.number(), - }).allow(null), - }) - ) - .allow(null), - }), - }, - }, - handler: async (req: any, h: any) => { - try { - const report = req.payload; - await storeReport(server, report); - return { status: 'ok' }; - } catch (error) { - return { status: 'fail' }; - } - }, - }); -} diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js index faccdf396df043..e2a70b94c0010c 100644 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ b/src/legacy/plugin_discovery/find_plugin_specs.js @@ -21,7 +21,7 @@ import * as Rx from 'rxjs'; import { distinct, toArray, mergeMap, share, shareReplay, filter, last, map, tap } from 'rxjs/operators'; import { realpathSync } from 'fs'; -import { transformDeprecations, Config } from '../server/config'; +import { Config } from '../server/config'; import { extendConfigService, @@ -40,9 +40,7 @@ import { } from './errors'; export function defaultConfig(settings) { - return Config.withDefaultSchema( - transformDeprecations(settings) - ); + return Config.withDefaultSchema(settings); } function bufferAllResults(observable) { diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js index d7d32ca04976ad..44ecb5718fe212 100644 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ b/src/legacy/plugin_discovery/plugin_config/settings.js @@ -19,7 +19,6 @@ import { get } from 'lodash'; -import * as serverConfig from '../../server/config'; import { getTransform } from '../../deprecation'; /** @@ -33,7 +32,7 @@ import { getTransform } from '../../deprecation'; */ export async function getSettings(spec, rootSettings, logDeprecation) { const prefix = spec.getConfigPrefix(); - const rawSettings = get(serverConfig.transformDeprecations(rootSettings), prefix); + const rawSettings = get(rootSettings, prefix); const transform = await getTransform(spec); return transform(rawSettings, logDeprecation); } diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index c32418e1aeb62e..fe886b9d178111 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -69,6 +69,7 @@ export interface LegacyPluginOptions { noParse: string[]; home: string[]; mappings: any; + migrations: any; savedObjectSchemas: SavedObjectsSchemaDefinition; savedObjectsManagement: SavedObjectsManagementDefinition; visTypes: string[]; diff --git a/src/legacy/server/config/__tests__/deprecation_warnings.js b/src/legacy/server/config/__tests__/deprecation_warnings.js deleted file mode 100644 index f49a1b6df45e21..00000000000000 --- a/src/legacy/server/config/__tests__/deprecation_warnings.js +++ /dev/null @@ -1,116 +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 { spawn } from 'child_process'; - -import expect from '@kbn/expect'; - -const RUN_KBN_SERVER_STARTUP = require.resolve('./fixtures/run_kbn_server_startup'); -const SETUP_NODE_ENV = require.resolve('../../../../setup_node_env'); -const SECOND = 1000; - -describe('config/deprecation warnings', function () { - this.timeout(65 * SECOND); - - let stdio = ''; - let proc = null; - - before(async () => { - proc = spawn(process.execPath, [ - '-r', SETUP_NODE_ENV, - RUN_KBN_SERVER_STARTUP - ], { - stdio: ['ignore', 'pipe', 'pipe'], - env: { - ...process.env, - CREATE_SERVER_OPTS: JSON.stringify({ - logging: { - quiet: false, - silent: false - }, - uiSettings: { - enabled: true - } - }) - } - }); - - // Either time out in 60 seconds, or resolve once the line is in our buffer - return Promise.race([ - new Promise((resolve) => setTimeout(resolve, 60 * SECOND)), - new Promise((resolve, reject) => { - proc.stdout.on('data', (chunk) => { - stdio += chunk.toString('utf8'); - if (chunk.toString('utf8').includes('deprecation')) { - resolve(); - } - }); - - proc.stderr.on('data', (chunk) => { - stdio += chunk.toString('utf8'); - if (chunk.toString('utf8').includes('deprecation')) { - resolve(); - } - }); - - proc.on('exit', (code) => { - proc = null; - if (code > 0) { - reject(new Error(`Kibana server exited with ${code} -- stdout:\n\n${stdio}\n`)); - } else { - resolve(); - } - }); - }) - ]); - }); - - after(() => { - if (proc) { - proc.kill('SIGKILL'); - } - }); - - it('logs deprecation warnings when using outdated config', async () => { - const deprecationLines = stdio - .split('\n') - .map(json => { - try { - // in dev mode kibana might log things like node.js warnings which - // are not JSON, ignore the lines that don't parse as JSON - return JSON.parse(json); - } catch (error) { - return null; - } - }) - .filter(Boolean) - .filter(line => - line.type === 'log' && - line.tags.includes('deprecation') && - line.tags.includes('warning') - ); - - try { - expect(deprecationLines).to.have.length(1); - expect(deprecationLines[0]).to.have.property('message', 'uiSettings.enabled is deprecated and is no longer used'); - } catch (error) { - throw new Error(`Expected stdio to include deprecation message about uiSettings.enabled\n\nstdio:\n${stdio}\n\n`); - } - }); -}); diff --git a/src/legacy/server/config/index.js b/src/legacy/server/config/index.js index 7cc53ae1c74fbf..0a83ecb1c58e1c 100644 --- a/src/legacy/server/config/index.js +++ b/src/legacy/server/config/index.js @@ -17,5 +17,4 @@ * under the License. */ -export { transformDeprecations } from './transform_deprecations'; export { Config } from './config'; diff --git a/src/legacy/server/config/transform_deprecations.js b/src/legacy/server/config/transform_deprecations.js deleted file mode 100644 index b23a1de2c07732..00000000000000 --- a/src/legacy/server/config/transform_deprecations.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { createTransform, Deprecations } from '../../deprecation'; - -const { rename, unused } = Deprecations; - -const savedObjectsIndexCheckTimeout = (settings, log) => { - if (_.has(settings, 'savedObjects.indexCheckTimeout')) { - log('savedObjects.indexCheckTimeout is no longer necessary.'); - - if (Object.keys(settings.savedObjects).length > 1) { - delete settings.savedObjects.indexCheckTimeout; - } else { - delete settings.savedObjects; - } - } -}; - -const rewriteBasePath = (settings, log) => { - if (_.has(settings, 'server.basePath') && !_.has(settings, 'server.rewriteBasePath')) { - log( - 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + - 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + - 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + - 'current behavior and silence this warning.' - ); - } -}; - -const configPath = (settings, log) => { - if (_.has(process, 'env.CONFIG_PATH')) { - log(`Environment variable CONFIG_PATH is deprecated. It has been replaced with KIBANA_PATH_CONF pointing to a config folder`); - } -}; - -const dataPath = (settings, log) => { - if (_.has(process, 'env.DATA_PATH')) { - log(`Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`); - } -}; - -const NONCE_STRING = `{nonce}`; -// Policies that should include the 'self' source -const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); -const SELF_STRING = `'self'`; - -const cspRules = (settings, log) => { - const rules = _.get(settings, 'csp.rules'); - if (!rules) { - return; - } - - const parsed = new Map(rules.map(ruleStr => { - const parts = ruleStr.split(/\s+/); - return [parts[0], parts.slice(1)]; - })); - - settings.csp.rules = [...parsed].map(([policy, sourceList]) => { - if (sourceList.find(source => source.includes(NONCE_STRING))) { - log(`csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`); - sourceList = sourceList.filter(source => !source.includes(NONCE_STRING)); - - // Add 'self' if not present - if (!sourceList.find(source => source.includes(SELF_STRING))) { - sourceList.push(SELF_STRING); - } - } - - if (SELF_POLICIES.includes(policy) && !sourceList.find(source => source.includes(SELF_STRING))) { - log(`csp.rules must contain the 'self' source. Automatically adding to ${policy}.`); - sourceList.push(SELF_STRING); - } - - return `${policy} ${sourceList.join(' ')}`.trim(); - }); -}; - -const deprecations = [ - //server - unused('server.xsrf.token'), - unused('uiSettings.enabled'), - rename('optimize.lazy', 'optimize.watch'), - rename('optimize.lazyPort', 'optimize.watchPort'), - rename('optimize.lazyHost', 'optimize.watchHost'), - rename('optimize.lazyPrebuild', 'optimize.watchPrebuild'), - rename('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - rename('xpack.telemetry.enabled', 'telemetry.enabled'), - rename('xpack.telemetry.config', 'telemetry.config'), - rename('xpack.telemetry.banner', 'telemetry.banner'), - rename('xpack.telemetry.url', 'telemetry.url'), - savedObjectsIndexCheckTimeout, - rewriteBasePath, - configPath, - dataPath, - cspRules -]; - -export const transformDeprecations = createTransform(deprecations); diff --git a/src/legacy/server/config/transform_deprecations.test.js b/src/legacy/server/config/transform_deprecations.test.js deleted file mode 100644 index f8cf38efc8bd8a..00000000000000 --- a/src/legacy/server/config/transform_deprecations.test.js +++ /dev/null @@ -1,182 +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'; -import { transformDeprecations } from './transform_deprecations'; - -describe('server/config', function () { - describe('transformDeprecations', function () { - describe('savedObjects.indexCheckTimeout', () => { - it('removes the indexCheckTimeout and savedObjects properties', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - }, - }; - - expect(transformDeprecations(settings)).toEqual({}); - }); - - it('keeps the savedObjects property if it has other keys', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - foo: 'bar', - }, - }; - - expect(transformDeprecations(settings)).toEqual({ - savedObjects: { - foo: 'bar', - }, - }); - }); - - it('logs that the setting is no longer necessary', () => { - const settings = { - savedObjects: { - indexCheckTimeout: 123, - }, - }; - - const log = sinon.spy(); - transformDeprecations(settings, log); - sinon.assert.calledOnce(log); - sinon.assert.calledWithExactly(log, sinon.match('savedObjects.indexCheckTimeout')); - }); - }); - - describe('csp.rules', () => { - describe('with nonce source', () => { - it('logs a warning', () => { - const settings = { - csp: { - rules: [`script-src 'self' 'nonce-{nonce}'`], - }, - }; - - const log = jest.fn(); - transformDeprecations(settings, log); - expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", - ], - ] - `); - }); - - it('replaces a nonce', () => { - expect( - transformDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }, jest.fn()).csp - .rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - - it('removes a quoted nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a non-quoted nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' nonce-{nonce}`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - expect( - transformDeprecations( - { csp: { rules: [`script-src nonce-{nonce} 'self'`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a strange nonce', () => { - expect( - transformDeprecations( - { csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes multiple nonces', () => { - expect( - transformDeprecations( - { - csp: { - rules: [ - `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, - `style-src 'nonce-{nonce}' 'self'`, - ], - }, - }, - jest.fn() - ).csp.rules - ).toEqual([`script-src 'self'`, `style-src 'self'`]); - }); - }); - - describe('without self source', () => { - it('logs a warning', () => { - const log = jest.fn(); - transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, log); - expect(log.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "csp.rules must contain the 'self' source. Automatically adding to script-src.", - ], - ] - `); - }); - - it('adds self', () => { - expect( - transformDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }, jest.fn()).csp - .rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - }); - - it('does not add self to other policies', () => { - expect( - transformDeprecations({ csp: { rules: [`worker-src blob:`] } }, jest.fn()).csp.rules - ).toEqual([`worker-src blob:`]); - }); - }); - }); -}); diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 87fb9dc8b9fecb..ecd4dcfa14eb51 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -31,8 +31,6 @@ import { loggingMixin } from './logging'; import warningsMixin from './warnings'; import { statusMixin } from './status'; import pidMixin from './pid'; -import { configDeprecationWarningsMixin } from './config/deprecation_warnings'; -import { transformDeprecations } from './config/transform_deprecations'; import configCompleteMixin from './config/complete'; import optimizeMixin from '../../optimize'; import * as Plugins from './plugins'; @@ -89,7 +87,6 @@ export default class KbnServer { // adds methods for extending this.server serverExtensionsMixin, loggingMixin, - configDeprecationWarningsMixin, warningsMixin, statusMixin, @@ -198,7 +195,7 @@ export default class KbnServer { applyLoggingConfiguration(settings) { const config = new Config( this.config.getSchema(), - transformDeprecations(settings) + settings ); const loggingOptions = loggingConfiguration(config); diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/legacy/server/url_shortening/routes/create_routes.js index 091eabcf47c1fd..c6347ace873f73 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/legacy/server/url_shortening/routes/create_routes.js @@ -19,12 +19,10 @@ import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; -import { createShortenUrlRoute } from './shorten_url'; export function createRoutes(server) { const shortUrlLookup = shortUrlLookupProvider(server); server.route(createGotoRoute({ server, shortUrlLookup })); - server.route(createShortenUrlRoute({ shortUrlLookup })); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/legacy/server/url_shortening/routes/goto.js index 675bc5df506703..60a34499dd2d5a 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/legacy/server/url_shortening/routes/goto.js @@ -22,18 +22,12 @@ import { shortUrlAssertValid } from './lib/short_url_assert_valid'; export const createGotoRoute = ({ server, shortUrlLookup }) => ({ method: 'GET', - path: '/goto/{urlId}', + path: '/goto_LP/{urlId}', handler: async function (request, h) { try { const url = await shortUrlLookup.getUrl(request.params.urlId, request); shortUrlAssertValid(url); - const uiSettings = request.getUiSettingsService(); - const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); - if (!stateStoreInSessionStorage) { - return h.redirect(request.getBasePath() + url); - } - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); return h.renderApp(app, { redirectUrl: url, diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js index c4f6af03d7d934..3a4b96c802c58b 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js @@ -17,7 +17,6 @@ * under the License. */ -import crypto from 'crypto'; import { get } from 'lodash'; export function shortUrlLookupProvider(server) { @@ -34,29 +33,6 @@ export function shortUrlLookupProvider(server) { } return { - async generateUrlId(url, req) { - const id = crypto.createHash('md5').update(url).digest('hex'); - const savedObjectsClient = req.getSavedObjectsClient(); - const { isConflictError } = savedObjectsClient.errors; - - try { - const doc = await savedObjectsClient.create('url', { - url, - accessCount: 0, - createDate: new Date(), - accessDate: new Date() - }, { id }); - - return doc.id; - } catch (error) { - if (isConflictError(error)) { - return id; - } - - throw error; - } - }, - async getUrl(id, req) { const doc = await req.getSavedObjectsClient().get('url', id); updateMetadata(doc, req); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js index 033aeb92926a51..7303682c63e0b1 100644 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js @@ -48,43 +48,6 @@ describe('shortUrlLookupProvider', () => { sandbox.restore(); }); - describe('generateUrlId', () => { - it('returns the document id', async () => { - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - - it('provides correct arguments to savedObjectsClient', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes, options] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - expect(options.id).toEqual(ID); - }); - - it('passes persists attributes', async () => { - await shortUrl.generateUrlId(URL, req); - - sinon.assert.calledOnce(savedObjectsClient.create); - const [type, attributes] = savedObjectsClient.create.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate', 'createDate', 'url']); - expect(attributes.url).toEqual(URL); - }); - - it('gracefully handles version conflict', async () => { - const error = savedObjectsClient.errors.decorateConflictError(new Error()); - savedObjectsClient.create.throws(error); - const id = await shortUrl.generateUrlId(URL, req); - expect(id).toEqual(ID); - }); - }); - describe('getUrl', () => { beforeEach(() => { const attributes = { accessCount: 2, url: URL }; diff --git a/src/legacy/ui/public/agg_types/agg_config.ts b/src/legacy/ui/public/agg_types/agg_config.ts index d4ef203721456d..9306ffcaff9fd9 100644 --- a/src/legacy/ui/public/agg_types/agg_config.ts +++ b/src/legacy/ui/public/agg_types/agg_config.ts @@ -29,7 +29,6 @@ import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; import { SearchSourceContract, FetchOptions } from '../courier/types'; import { AggType } from './agg_type'; -import { FieldParamType } from './param_types/field'; import { AggGroupNames } from '../vis/editors/default/agg_groups'; import { writeParams } from './agg_params'; import { AggConfigs } from './agg_configs'; @@ -204,7 +203,7 @@ export class AggConfig { } write(aggs?: AggConfigs) { - return writeParams(this.type.params, this, aggs); + return writeParams(this.type.params, this, aggs); } isFilterable() { @@ -425,13 +424,15 @@ export class AggConfig { } this.__type = type; + let availableFields = []; + + const fieldParam = this.type && this.type.params.find((p: any) => p.type === 'field'); + + if (fieldParam) { + // @ts-ignore + availableFields = fieldParam.getAvailableFields(this.getIndexPattern().fields); + } - const fieldParam = - this.type && (this.type.params.find((p: any) => p.type === 'field') as FieldParamType); - // @ts-ignore - const availableFields = fieldParam - ? fieldParam.getAvailableFields(this.getIndexPattern().fields) - : []; // clear out the previous params except for a few special ones this.setParams({ // split row/columns is "outside" of the agg, so don't reset it diff --git a/src/legacy/ui/public/agg_types/agg_configs.ts b/src/legacy/ui/public/agg_types/agg_configs.ts index b4ea0ec8bc465c..b5a7474c99b0e7 100644 --- a/src/legacy/ui/public/agg_types/agg_configs.ts +++ b/src/legacy/ui/public/agg_types/agg_configs.ts @@ -126,7 +126,10 @@ export class AggConfigs { return aggConfigs; } - createAggConfig(params: AggConfig | AggConfigOptions, { addToAggConfigs = true } = {}) { + createAggConfig( + params: AggConfig | AggConfigOptions, + { addToAggConfigs = true } = {} + ) { let aggConfig; if (params instanceof AggConfig) { aggConfig = params; @@ -137,7 +140,7 @@ export class AggConfigs { if (addToAggConfigs) { this.aggs.push(aggConfig); } - return aggConfig; + return aggConfig as T; } /** diff --git a/src/legacy/ui/public/agg_types/agg_params.test.ts b/src/legacy/ui/public/agg_types/agg_params.test.ts index 28d852c7f2567d..25e62e06d52d70 100644 --- a/src/legacy/ui/public/agg_types/agg_params.test.ts +++ b/src/legacy/ui/public/agg_types/agg_params.test.ts @@ -17,17 +17,18 @@ * under the License. */ -import { AggParam, initParams } from './agg_params'; +import { initParams } from './agg_params'; import { BaseParamType } from './param_types/base'; import { FieldParamType } from './param_types/field'; import { OptionedParamType } from './param_types/optioned'; +import { AggParamType } from '../agg_types/param_types/agg'; jest.mock('ui/new_platform'); describe('AggParams class', () => { describe('constructor args', () => { it('accepts an array of param defs', () => { - const params = [{ name: 'one' }, { name: 'two' }] as AggParam[]; + const params = [{ name: 'one' }, { name: 'two' }] as AggParamType[]; const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); @@ -37,7 +38,7 @@ describe('AggParams class', () => { describe('AggParam creation', () => { it('Uses the FieldParamType class for params with the name "field"', () => { - const params = [{ name: 'field', type: 'field' }] as AggParam[]; + const params = [{ name: 'field', type: 'field' }] as AggParamType[]; const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); @@ -50,7 +51,7 @@ describe('AggParams class', () => { name: 'order', type: 'optioned', }, - ]; + ] as AggParamType[]; const aggParams = initParams(params); expect(aggParams).toHaveLength(params.length); @@ -71,7 +72,7 @@ describe('AggParams class', () => { name: 'waist', displayName: 'waist', }, - ] as AggParam[]; + ] as AggParamType[]; const aggParams = initParams(params); diff --git a/src/legacy/ui/public/agg_types/agg_params.ts b/src/legacy/ui/public/agg_types/agg_params.ts index 8925bf9e3d46c6..262a57f4a5aa30 100644 --- a/src/legacy/ui/public/agg_types/agg_params.ts +++ b/src/legacy/ui/public/agg_types/agg_params.ts @@ -44,17 +44,10 @@ export interface AggParamOption { enabled?(agg: AggConfig): boolean; } -export interface AggParamConfig { - type: string; -} - -export const initParams = < - TAggParam extends AggParam = AggParam, - TAggParamConfig extends AggParamConfig = AggParamConfig ->( - params: TAggParamConfig[] +export const initParams = ( + params: TAggParam[] ): TAggParam[] => - params.map((config: TAggParamConfig) => { + params.map((config: TAggParam) => { const Class = paramTypeMap[config.type] || paramTypeMap._default; return new Class(config); @@ -74,20 +67,25 @@ export const initParams = < * output object which is used to create the agg dsl for the search request. All other properties * are dependent on the AggParam#write methods which should be studied for each AggType. */ -export const writeParams = ( - params: AggParam[], - aggConfig: AggConfig, +export const writeParams = < + TAggConfig extends AggConfig = AggConfig, + TAggParam extends AggParamType = AggParamType +>( + params: Array> = [], + aggConfig: TAggConfig, aggs?: AggConfigs, locals?: Record ) => { const output = { params: {} as Record }; locals = locals || {}; - params.forEach(function(param) { + params.forEach(param => { if (param.write) { param.write(aggConfig, output, aggs, locals); } else { - output.params[param.name] = aggConfig.params[param.name]; + if (param && param.name) { + output.params[param.name] = aggConfig.params[param.name]; + } } }); diff --git a/src/legacy/ui/public/agg_types/agg_type.ts b/src/legacy/ui/public/agg_types/agg_type.ts index 5216affb3e52dc..ff4c6875ec6c06 100644 --- a/src/legacy/ui/public/agg_types/agg_type.ts +++ b/src/legacy/ui/public/agg_types/agg_type.ts @@ -20,19 +20,19 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; -import { AggParam, initParams } from './agg_params'; +import { initParams } from './agg_params'; import { AggConfig } from '../vis'; import { AggConfigs } from './agg_configs'; import { SearchSource } from '../courier'; import { Adapters } from '../inspector'; import { BaseParamType } from './param_types/base'; - +import { AggParamType } from '../agg_types/param_types/agg'; import { KBN_FIELD_TYPES, FieldFormat } from '../../../../plugins/data/public'; export interface AggTypeConfig< TAggConfig extends AggConfig = AggConfig, - TParam extends AggParam = AggParam + TParam extends AggParamType = AggParamType > { name: string; title: string; @@ -46,7 +46,7 @@ export interface AggTypeConfig< getRequestAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); getResponseAggs?: ((aggConfig: TAggConfig) => TAggConfig[]) | (() => TAggConfig[] | void); customLabels?: boolean; - decorateAggConfig?: () => Record; + decorateAggConfig?: () => any; postFlightRequest?: ( resp: any, aggConfigs: AggConfigs, @@ -67,7 +67,10 @@ const getFormat = (agg: AggConfig) => { return field ? field.format : fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING); }; -export class AggType { +export class AggType< + TAggConfig extends AggConfig = AggConfig, + TParam extends AggParamType = AggParamType +> { /** * the unique, unchanging, name that we have assigned this aggType * @@ -162,7 +165,7 @@ export class AggType Record; + decorateAggConfig: () => any; /** * A function that needs to be called after the main request has been made * and should return an updated response @@ -196,7 +199,7 @@ export class AggType any; paramByName = (name: string) => { - return this.params.find((p: AggParam) => p.name === name); + return this.params.find((p: TParam) => p.name === name); }; /** diff --git a/src/legacy/ui/public/agg_types/buckets/_bucket_agg_type.ts b/src/legacy/ui/public/agg_types/buckets/_bucket_agg_type.ts index c151f9101d0901..ed332ea420bcca 100644 --- a/src/legacy/ui/public/agg_types/buckets/_bucket_agg_type.ts +++ b/src/legacy/ui/public/agg_types/buckets/_bucket_agg_type.ts @@ -17,29 +17,33 @@ * under the License. */ -import { AggParamType } from '../param_types/agg'; import { AggConfig } from '../../vis'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; import { AggType, AggTypeConfig } from '../agg_type'; +import { AggParamType } from '../param_types/agg'; -export type IBucketAggConfig = AggConfig; +export interface IBucketAggConfig extends AggConfig { + type: InstanceType; +} -export interface BucketAggParam extends AggParamType { +export interface BucketAggParam + extends AggParamType { scriptable?: boolean; filterFieldTypes?: KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; } -export interface BucketAggTypeConfig - extends AggTypeConfig { +const bucketType = 'buckets'; + +interface BucketAggTypeConfig + extends AggTypeConfig> { getKey?: (bucket: any, key: any, agg: AggConfig) => any; } -const bucketType = 'buckets'; - -export class BucketAggType< - TBucketAggConfig extends IBucketAggConfig = IBucketAggConfig -> extends AggType { - getKey: (bucket: any, key: any, agg: IBucketAggConfig) => any; +export class BucketAggType extends AggType< + TBucketAggConfig, + BucketAggParam +> { + getKey: (bucket: any, key: any, agg: TBucketAggConfig) => any; type = bucketType; constructor(config: BucketAggTypeConfig) { diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/date_range.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/date_range.test.ts index 0399e8d3823200..ddb4102563a7c0 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/date_range.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/date_range.test.ts @@ -22,6 +22,7 @@ import { createFilterDateRange } from './date_range'; import { DateFormat } from '../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; +import { IBucketAggConfig } from '../_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -61,7 +62,7 @@ describe('AggConfig Filters', () => { const aggConfigs = getAggConfigs(); const from = new Date('1 Feb 2015'); const to = new Date('7 Feb 2015'); - const filter = createFilterDateRange(aggConfigs.aggs[0], { + const filter = createFilterDateRange(aggConfigs.aggs[0] as IBucketAggConfig, { from: from.valueOf(), to: to.valueOf(), }); diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/filters.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/filters.test.ts index 125532fe070ba8..34cf996826865f 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/filters.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/filters.test.ts @@ -18,6 +18,7 @@ */ import { createFilterFilters } from './filters'; import { AggConfigs } from '../../agg_configs'; +import { IBucketAggConfig } from '../_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -56,7 +57,7 @@ describe('AggConfig Filters', () => { }; it('should return a filters filter', () => { const aggConfigs = getAggConfigs(); - const filter = createFilterFilters(aggConfigs.aggs[0], 'type:nginx'); + const filter = createFilterFilters(aggConfigs.aggs[0] as IBucketAggConfig, 'type:nginx'); expect(filter!.query.bool.must[0].query_string.query).toBe('type:nginx'); expect(filter!.meta).toHaveProperty('index', '1234'); diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/histogram.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/histogram.test.ts index ac8e55f096fb41..d07cf84aef4d9b 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/histogram.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/histogram.test.ts @@ -19,6 +19,7 @@ import { createFilterHistogram } from './histogram'; import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; +import { IBucketAggConfig } from '../_bucket_agg_type'; import { BytesFormat } from '../../../../../../plugins/data/public'; jest.mock('ui/new_platform'); @@ -59,7 +60,7 @@ describe('AggConfig Filters', () => { it('should return an range filter for histogram', () => { const aggConfigs = getAggConfigs(); - const filter = createFilterHistogram(aggConfigs.aggs[0], '2048'); + const filter = createFilterHistogram(aggConfigs.aggs[0] as IBucketAggConfig, '2048'); expect(filter).toHaveProperty('meta'); expect(filter.meta).toHaveProperty('index', '1234'); diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.test.ts index 569735a60298de..bf6b437f17cf22 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.test.ts @@ -21,6 +21,7 @@ import { createFilterIpRange } from './ip_range'; import { AggConfigs } from '../../agg_configs'; import { IpFormat } from '../../../../../../plugins/data/public'; import { BUCKET_TYPES } from '../bucket_agg_types'; +import { IBucketAggConfig } from '../_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -59,7 +60,7 @@ describe('AggConfig Filters', () => { }, ]); - const filter = createFilterIpRange(aggConfigs.aggs[0], { + const filter = createFilterIpRange(aggConfigs.aggs[0] as IBucketAggConfig, { type: 'range', from: '0.0.0.0', to: '1.1.1.1', @@ -88,7 +89,7 @@ describe('AggConfig Filters', () => { }, ]); - const filter = createFilterIpRange(aggConfigs.aggs[0], { + const filter = createFilterIpRange(aggConfigs.aggs[0] as IBucketAggConfig, { type: 'mask', mask: '67.129.65.201/27', }); diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts index 803f6d97ae42d3..a513b8c7827398 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/ip_range.ts @@ -17,7 +17,7 @@ * under the License. */ -import { CidrMask } from '../../../utils/cidr_mask'; +import { CidrMask } from '../lib/cidr_mask'; import { IBucketAggConfig } from '../_bucket_agg_type'; import { IpRangeKey } from '../ip_range'; import { esFilters } from '../../../../../../plugins/data/public'; diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/range.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/range.test.ts index e7344f16ba0b16..dc02b773edc42e 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/range.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/range.test.ts @@ -21,6 +21,7 @@ import { createFilterRange } from './range'; import { BytesFormat } from '../../../../../../plugins/data/public'; import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; +import { IBucketAggConfig } from '../_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -60,7 +61,10 @@ describe('AggConfig Filters', () => { it('should return a range filter for range agg', () => { const aggConfigs = getAggConfigs(); - const filter = createFilterRange(aggConfigs.aggs[0], { gte: 1024, lt: 2048.0 }); + const filter = createFilterRange(aggConfigs.aggs[0] as IBucketAggConfig, { + gte: 1024, + lt: 2048.0, + }); expect(filter).toHaveProperty('range'); expect(filter).toHaveProperty('meta'); diff --git a/src/legacy/ui/public/agg_types/buckets/create_filter/terms.test.ts b/src/legacy/ui/public/agg_types/buckets/create_filter/terms.test.ts index 42f8349d5a2a3d..86c0aa24f529a6 100644 --- a/src/legacy/ui/public/agg_types/buckets/create_filter/terms.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/create_filter/terms.test.ts @@ -20,6 +20,7 @@ import { createFilterTerms } from './terms'; import { AggConfigs } from '../../agg_configs'; import { BUCKET_TYPES } from '../bucket_agg_types'; +import { IBucketAggConfig } from '../_bucket_agg_type'; import { esFilters } from '../../../../../../plugins/data/public'; jest.mock('ui/new_platform'); @@ -49,7 +50,11 @@ describe('AggConfig Filters', () => { { type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } }, ]); - const filter = createFilterTerms(aggConfigs.aggs[0], 'apache', {}) as esFilters.Filter; + const filter = createFilterTerms( + aggConfigs.aggs[0] as IBucketAggConfig, + 'apache', + {} + ) as esFilters.Filter; expect(filter).toHaveProperty('query'); expect(filter.query).toHaveProperty('match_phrase'); @@ -64,27 +69,35 @@ describe('AggConfig Filters', () => { { type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } }, ]); - const filterFalse = createFilterTerms(aggConfigs.aggs[0], '', {}) as esFilters.Filter; + const filterFalse = createFilterTerms( + aggConfigs.aggs[0] as IBucketAggConfig, + '', + {} + ) as esFilters.Filter; expect(filterFalse).toHaveProperty('query'); expect(filterFalse.query).toHaveProperty('match_phrase'); expect(filterFalse.query.match_phrase).toHaveProperty('field'); expect(filterFalse.query.match_phrase.field).toBeFalsy(); - const filterTrue = createFilterTerms(aggConfigs.aggs[0], '1', {}) as esFilters.Filter; + const filterTrue = createFilterTerms( + aggConfigs.aggs[0] as IBucketAggConfig, + '1', + {} + ) as esFilters.Filter; expect(filterTrue).toHaveProperty('query'); expect(filterTrue.query).toHaveProperty('match_phrase'); expect(filterTrue.query.match_phrase).toHaveProperty('field'); expect(filterTrue.query.match_phrase.field).toBeTruthy(); }); - // + it('should generate correct __missing__ filter', () => { const aggConfigs = getAggConfigs([ { type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } }, ]); const filter = createFilterTerms( - aggConfigs.aggs[0], + aggConfigs.aggs[0] as IBucketAggConfig, '__missing__', {} ) as esFilters.ExistsFilter; @@ -95,13 +108,13 @@ describe('AggConfig Filters', () => { expect(filter.meta).toHaveProperty('index', '1234'); expect(filter.meta).toHaveProperty('negate', true); }); - // + it('should generate correct __other__ filter', () => { const aggConfigs = getAggConfigs([ { type: BUCKET_TYPES.TERMS, schema: 'segment', params: { field: 'field' } }, ]); - const [filter] = createFilterTerms(aggConfigs.aggs[0], '__other__', { + const [filter] = createFilterTerms(aggConfigs.aggs[0] as IBucketAggConfig, '__other__', { terms: ['apache'], }) as esFilters.Filter[]; diff --git a/src/legacy/ui/public/agg_types/buckets/date_histogram.ts b/src/legacy/ui/public/agg_types/buckets/date_histogram.ts index 6a87b2e88ac4cc..45122a24c81844 100644 --- a/src/legacy/ui/public/agg_types/buckets/date_histogram.ts +++ b/src/legacy/ui/public/agg_types/buckets/date_histogram.ts @@ -20,9 +20,10 @@ import _ from 'lodash'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import { BUCKET_TYPES } from 'ui/agg_types/buckets/bucket_agg_types'; + import { npStart } from 'ui/new_platform'; -import { BucketAggParam, BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; +import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; +import { BUCKET_TYPES } from './bucket_agg_types'; import { createFilterDateHistogram } from './create_filter/date_histogram'; import { intervalOptions } from './_interval_options'; import { TimeIntervalParamEditor } from '../../vis/editors/default/controls/time_interval'; @@ -31,7 +32,6 @@ import { DropPartialsParamEditor } from '../../vis/editors/default/controls/drop import { ScaleMetricsParamEditor } from '../../vis/editors/default/controls/scale_metrics'; import { dateHistogramInterval } from '../../../../core_plugins/data/public'; import { writeParams } from '../agg_params'; -import { AggConfigs } from '../agg_configs'; import { isMetricAggType } from '../metrics/metric_agg_type'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; @@ -77,7 +77,12 @@ export const dateHistogramBucketAgg = new BucketAggType = writeParams(this.params as BucketAggParam[], agg); + let output: Record = {}; + + if (this.params) { + output = writeParams(this.params, agg); + } + const field = agg.getFieldDisplayName(); return i18n.translate('common.ui.aggTypes.buckets.dateHistogramLabel', { defaultMessage: '{fieldName} per {intervalDescription}', @@ -102,7 +107,7 @@ export const dateHistogramBucketAgg = new BucketAggType, aggs: AggConfigs) { + write(agg, output, aggs) { setBounds(agg, true); agg.buckets.setInterval(getInterval(agg)); const { useNormalizedEsInterval, scaleMetricValues } = agg.params; @@ -200,7 +205,7 @@ export const dateHistogramBucketAgg = new BucketAggType) { + write(agg, output) { // If a time_zone has been set explicitly always prefer this. let tz = agg.params.time_zone; if (!tz && agg.params.field) { @@ -230,7 +235,7 @@ export const dateHistogramBucketAgg = new BucketAggType) { + write(agg, output) { const val = agg.params.extended_bounds; if (val.min != null || val.max != null) { @@ -263,6 +268,6 @@ export const dateHistogramBucketAgg = new BucketAggType undefined, - write: (agg: IBucketAggConfig, output: Record) => { + write: (agg, output) => { const field = agg.getParam('field'); let tz = agg.getParam('time_zone'); @@ -114,3 +112,16 @@ export const dateRangeBucketAgg = new BucketAggType({ }, ], }); + +export const convertDateRangeToString = ( + { from, to }: DateRangeKey, + format: (val: any) => string +) => { + if (!from) { + return 'Before ' + format(to); + } else if (!to) { + return 'After ' + format(from); + } else { + return format(from) + ' to ' + format(to); + } +}; diff --git a/src/legacy/ui/public/agg_types/buckets/filters.ts b/src/legacy/ui/public/agg_types/buckets/filters.ts index caebf2d7d974ee..6e7f4e27b9e90d 100644 --- a/src/legacy/ui/public/agg_types/buckets/filters.ts +++ b/src/legacy/ui/public/agg_types/buckets/filters.ts @@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n'; import chrome from 'ui/chrome'; import { FiltersParamEditor, FilterValue } from '../../vis/editors/default/controls/filters'; import { createFilterFilters } from './create_filter/filters'; -import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; +import { BucketAggType } from './_bucket_agg_type'; import { Storage } from '../../../../../plugins/kibana_utils/public'; import { getQueryLog, esQuery } from '../../../../../plugins/data/public'; @@ -48,7 +48,7 @@ export const filtersBucketAgg = new BucketAggType({ name: 'filters', editorComponent: FiltersParamEditor, default: [{ input: { query: '', language: config.get('search:queryLanguage') }, label: '' }], - write(aggConfig: IBucketAggConfig, output: Record) { + write(aggConfig, output) { const inFilters: FilterValue[] = aggConfig.params.filters; if (!_.size(inFilters)) return; diff --git a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts index 700f5a048fce20..8e39a464b9adf0 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_hash.ts +++ b/src/legacy/ui/public/agg_types/buckets/geo_hash.ts @@ -18,18 +18,17 @@ */ import { i18n } from '@kbn/i18n'; +import { geohashColumns } from 'ui/vis/map/decode_geo_hash'; import chrome from '../../chrome'; import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { AutoPrecisionParamEditor } from '../../vis/editors/default/controls/auto_precision'; import { UseGeocentroidParamEditor } from '../../vis/editors/default/controls/use_geocentroid'; import { IsFilteredByCollarParamEditor } from '../../vis/editors/default/controls/is_filtered_by_collar'; import { PrecisionParamEditor } from '../../vis/editors/default/controls/precision'; -import { geohashColumns } from '../../utils/decode_geo_hash'; import { AggGroupNames } from '../../vis/editors/default/agg_groups'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; -// @ts-ignore -import { geoContains, scaleBounds } from '../../utils/geo_utils'; +import { geoContains, scaleBounds, GeoBoundingBox } from './lib/geo_utils'; import { BUCKET_TYPES } from './bucket_agg_types'; const config = chrome.getUiSettingsClient(); @@ -70,15 +69,15 @@ function getPrecision(val: string) { return precision; } -const isOutsideCollar = (bounds: unknown, collar: MapCollar) => +const isOutsideCollar = (bounds: GeoBoundingBox, collar: MapCollar) => bounds && collar && !geoContains(collar, bounds); const geohashGridTitle = i18n.translate('common.ui.aggTypes.buckets.geohashGridTitle', { defaultMessage: 'Geohash', }); -interface MapCollar { - zoom: unknown; +interface MapCollar extends GeoBoundingBox { + zoom?: unknown; } export interface IBucketGeoHashGridAggConfig extends IBucketAggConfig { @@ -142,17 +141,19 @@ export const geoHashBucketAgg = new BucketAggType({ }, ], getRequestAggs(agg) { - const aggs: IBucketAggConfig[] = []; + const aggs = []; const params = agg.params; if (params.isFilteredByCollar && agg.getField()) { const { mapBounds, mapZoom } = params; if (mapBounds) { - let mapCollar; + let mapCollar: MapCollar; + if ( - !agg.lastMapCollar || - agg.lastMapCollar.zoom !== mapZoom || - isOutsideCollar(mapBounds, agg.lastMapCollar) + mapBounds && + (!agg.lastMapCollar || + agg.lastMapCollar.zoom !== mapZoom || + isOutsideCollar(mapBounds, agg.lastMapCollar)) ) { mapCollar = scaleBounds(mapBounds); mapCollar.zoom = mapZoom; diff --git a/src/legacy/ui/public/agg_types/buckets/geo_tile.ts b/src/legacy/ui/public/agg_types/buckets/geo_tile.ts index 3afb35a0356900..ef71e3947566a7 100644 --- a/src/legacy/ui/public/agg_types/buckets/geo_tile.ts +++ b/src/legacy/ui/public/agg_types/buckets/geo_tile.ts @@ -19,12 +19,13 @@ import { i18n } from '@kbn/i18n'; import { noop } from 'lodash'; -import { METRIC_TYPES } from 'ui/agg_types/metrics/metric_agg_types'; -import { AggConfigOptions } from 'ui/agg_types/agg_config'; +import { AggConfigOptions } from '../agg_config'; import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; +import { IBucketAggConfig } from './_bucket_agg_type'; +import { METRIC_TYPES } from '../metrics/metric_agg_types'; const geotileGridTitle = i18n.translate('common.ui.aggTypes.buckets.geotileGridTitle', { defaultMessage: 'Geotile', @@ -67,6 +68,6 @@ export const geoTileBucketAgg = new BucketAggType({ aggs.push(agg.aggConfigs.createAggConfig(aggConfig, { addToAggConfigs: false })); } - return aggs; + return aggs as IBucketAggConfig[]; }, }); diff --git a/src/legacy/ui/public/agg_types/buckets/histogram.ts b/src/legacy/ui/public/agg_types/buckets/histogram.ts index 7bd3d565003bee..d287cbddb7834c 100644 --- a/src/legacy/ui/public/agg_types/buckets/histogram.ts +++ b/src/legacy/ui/public/agg_types/buckets/histogram.ts @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { toastNotifications } from 'ui/notify'; import { npStart } from 'ui/new_platform'; -import { BucketAggType, IBucketAggConfig, BucketAggParam } from './_bucket_agg_type'; +import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; import { createFilterHistogram } from './create_filter/histogram'; import { NumberIntervalParamEditor } from '../../vis/editors/default/controls/number_interval'; import { MinDocCountParamEditor } from '../../vis/editors/default/controls/min_doc_count'; @@ -130,7 +130,7 @@ export const histogramBucketAgg = new BucketAggType({ ); }); }, - write(aggConfig: IBucketHistogramAggConfig, output: Record) { + write(aggConfig, output) { let interval = parseFloat(aggConfig.params.interval); if (interval <= 0) { interval = 1; @@ -171,12 +171,12 @@ export const histogramBucketAgg = new BucketAggType({ output.params.interval = interval; }, - } as BucketAggParam, + }, { name: 'min_doc_count', default: false, editorComponent: MinDocCountParamEditor, - write(aggConfig: IBucketAggConfig, output: Record) { + write(aggConfig, output) { if (aggConfig.params.min_doc_count) { output.params.min_doc_count = 0; } else { @@ -197,7 +197,7 @@ export const histogramBucketAgg = new BucketAggType({ max: '', }, editorComponent: ExtendedBoundsParamEditor, - write(aggConfig: IBucketAggConfig, output: Record) { + write(aggConfig, output) { const { min, max } = aggConfig.params.extended_bounds; if (aggConfig.params.has_extended_bounds && (min || min === 0) && (max || max === 0)) { diff --git a/src/legacy/ui/public/agg_types/buckets/ip_range.ts b/src/legacy/ui/public/agg_types/buckets/ip_range.ts index 35155a482734ce..609cd8adb5c39e 100644 --- a/src/legacy/ui/public/agg_types/buckets/ip_range.ts +++ b/src/legacy/ui/public/agg_types/buckets/ip_range.ts @@ -20,10 +20,9 @@ import { noop, map, omit, isNull } from 'lodash'; import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; -import { BucketAggType, IBucketAggConfig } from './_bucket_agg_type'; +import { BucketAggType } from './_bucket_agg_type'; import { IpRangeTypeParamEditor } from '../../vis/editors/default/controls/ip_range_type'; import { IpRangesParamEditor } from '../../vis/editors/default/controls/ip_ranges'; -import { ipRange } from '../../utils/ip_range'; import { BUCKET_TYPES } from './bucket_agg_types'; // @ts-ignore @@ -59,7 +58,7 @@ export const ipRangeBucketAgg = new BucketAggType({ fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.IP) ); const IpRangeFormat = FieldFormat.from(function(range: IpRangeKey) { - return ipRange.toString(range, formatter); + return convertIPRangeToString(range, formatter); }); return new IpRangeFormat(); }, @@ -93,7 +92,7 @@ export const ipRangeBucketAgg = new BucketAggType({ mask: [{ mask: '0.0.0.0/1' }, { mask: '128.0.0.0/2' }], }, editorComponent: IpRangesParamEditor, - write(aggConfig: IBucketAggConfig, output: Record) { + write(aggConfig, output) { const ipRangeType = aggConfig.params.ipRangeType; let ranges = aggConfig.params.ranges[ipRangeType]; @@ -106,3 +105,13 @@ export const ipRangeBucketAgg = new BucketAggType({ }, ], }); + +export const convertIPRangeToString = (range: IpRangeKey, format: (val: any) => string) => { + if (range.type === 'mask') { + return format(range.mask); + } + const from = range.from ? format(range.from) : '-Infinity'; + const to = range.to ? format(range.to) : 'Infinity'; + + return `${from} to ${to}`; +}; diff --git a/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts new file mode 100644 index 00000000000000..01dd3ddf1b8742 --- /dev/null +++ b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.test.ts @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CidrMask } from './cidr_mask'; + +describe('CidrMask', () => { + test('should throw errors with invalid CIDR masks', () => { + expect( + () => + // @ts-ignore + new CidrMask() + ).toThrowError(); + + expect(() => new CidrMask('')).toThrowError(); + expect(() => new CidrMask('hello, world')).toThrowError(); + expect(() => new CidrMask('0.0.0.0')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/0')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/33')).toThrowError(); + expect(() => new CidrMask('256.0.0.0/32')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/32/32')).toThrowError(); + expect(() => new CidrMask('1.2.3/1')).toThrowError(); + expect(() => new CidrMask('0.0.0.0/123d')).toThrowError(); + }); + + test('should correctly grab IP address and prefix length', () => { + let mask = new CidrMask('0.0.0.0/1'); + expect(mask.initialAddress.toString()).toBe('0.0.0.0'); + expect(mask.prefixLength).toBe(1); + + mask = new CidrMask('128.0.0.1/31'); + expect(mask.initialAddress.toString()).toBe('128.0.0.1'); + expect(mask.prefixLength).toBe(31); + }); + + test('should calculate a range of IP addresses', () => { + let mask = new CidrMask('0.0.0.0/1'); + let range = mask.getRange(); + expect(range.from.toString()).toBe('0.0.0.0'); + expect(range.to.toString()).toBe('127.255.255.255'); + + mask = new CidrMask('1.2.3.4/2'); + range = mask.getRange(); + expect(range.from.toString()).toBe('0.0.0.0'); + expect(range.to.toString()).toBe('63.255.255.255'); + + mask = new CidrMask('67.129.65.201/27'); + range = mask.getRange(); + expect(range.from.toString()).toBe('67.129.65.192'); + expect(range.to.toString()).toBe('67.129.65.223'); + }); + + test('toString()', () => { + let mask = new CidrMask('.../1'); + expect(mask.toString()).toBe('0.0.0.0/1'); + + mask = new CidrMask('128.0.0.1/31'); + expect(mask.toString()).toBe('128.0.0.1/31'); + }); +}); diff --git a/src/legacy/ui/public/utils/cidr_mask.ts b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts similarity index 96% rename from src/legacy/ui/public/utils/cidr_mask.ts rename to src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts index 249c60dedfebf6..aadbbc8c822768 100644 --- a/src/legacy/ui/public/utils/cidr_mask.ts +++ b/src/legacy/ui/public/agg_types/buckets/lib/cidr_mask.ts @@ -17,7 +17,8 @@ * under the License. */ -import { Ipv4Address } from '../../../../plugins/kibana_utils/public'; +import { Ipv4Address } from '../../../../../../plugins/kibana_utils/public'; + const NUM_BITS = 32; function throwError(mask: string) { diff --git a/src/legacy/ui/public/utils/geo_utils.js b/src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts similarity index 55% rename from src/legacy/ui/public/utils/geo_utils.js rename to src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts index 44b7670d16c111..639b6d1fbb03e3 100644 --- a/src/legacy/ui/public/utils/geo_utils.js +++ b/src/legacy/ui/public/agg_types/buckets/lib/geo_utils.ts @@ -19,46 +19,57 @@ import _ from 'lodash'; -export function geoContains(collar, bounds) { - //test if bounds top_left is outside collar - if(bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { +interface GeoBoundingBoxCoordinate { + lat: number; + lon: number; +} + +export interface GeoBoundingBox { + top_left: GeoBoundingBoxCoordinate; + bottom_right: GeoBoundingBoxCoordinate; +} + +export function geoContains(collar: GeoBoundingBox, bounds: GeoBoundingBox) { + // test if bounds top_left is outside collar + if (bounds.top_left.lat > collar.top_left.lat || bounds.top_left.lon < collar.top_left.lon) { return false; } - //test if bounds bottom_right is outside collar - if(bounds.bottom_right.lat < collar.bottom_right.lat || bounds.bottom_right.lon > collar.bottom_right.lon) { + // test if bounds bottom_right is outside collar + if ( + bounds.bottom_right.lat < collar.bottom_right.lat || + bounds.bottom_right.lon > collar.bottom_right.lon + ) { return false; } - //both corners are inside collar so collar contains bounds + // both corners are inside collar so collar contains bounds return true; } -export function scaleBounds(bounds) { - if (!bounds) return; - - const scale = .5; // scale bounds by 50% +export function scaleBounds(bounds: GeoBoundingBox): GeoBoundingBox { + const scale = 0.5; // scale bounds by 50% const topLeft = bounds.top_left; const bottomRight = bounds.bottom_right; let latDiff = _.round(Math.abs(topLeft.lat - bottomRight.lat), 5); const lonDiff = _.round(Math.abs(bottomRight.lon - topLeft.lon), 5); - //map height can be zero when vis is first created - if(latDiff === 0) latDiff = lonDiff; + // map height can be zero when vis is first created + if (latDiff === 0) latDiff = lonDiff; const latDelta = latDiff * scale; let topLeftLat = _.round(topLeft.lat, 5) + latDelta; - if(topLeftLat > 90) topLeftLat = 90; + if (topLeftLat > 90) topLeftLat = 90; let bottomRightLat = _.round(bottomRight.lat, 5) - latDelta; - if(bottomRightLat < -90) bottomRightLat = -90; + if (bottomRightLat < -90) bottomRightLat = -90; const lonDelta = lonDiff * scale; let topLeftLon = _.round(topLeft.lon, 5) - lonDelta; - if(topLeftLon < -180) topLeftLon = -180; + if (topLeftLon < -180) topLeftLon = -180; let bottomRightLon = _.round(bottomRight.lon, 5) + lonDelta; - if(bottomRightLon > 180) bottomRightLon = 180; + if (bottomRightLon > 180) bottomRightLon = 180; return { - 'top_left': { lat: topLeftLat, lon: topLeftLon }, - 'bottom_right': { lat: bottomRightLat, lon: bottomRightLon } + top_left: { lat: topLeftLat, lon: topLeftLon }, + bottom_right: { lat: bottomRightLat, lon: bottomRightLon }, }; } diff --git a/src/legacy/ui/public/agg_types/buckets/migrate_include_exclude_format.ts b/src/legacy/ui/public/agg_types/buckets/migrate_include_exclude_format.ts index e4527ff87f48cc..77e84e044de55a 100644 --- a/src/legacy/ui/public/agg_types/buckets/migrate_include_exclude_format.ts +++ b/src/legacy/ui/public/agg_types/buckets/migrate_include_exclude_format.ts @@ -19,9 +19,10 @@ import { isString, isObject } from 'lodash'; import { IBucketAggConfig, BucketAggType, BucketAggParam } from './_bucket_agg_type'; +import { AggConfig } from '../agg_config'; export const isType = (type: string) => { - return (agg: IBucketAggConfig): boolean => { + return (agg: AggConfig): boolean => { const field = agg.params.field; return field && field.type === type; @@ -31,7 +32,7 @@ export const isType = (type: string) => { export const isStringType = isType('string'); export const migrateIncludeExcludeFormat = { - serialize(this: BucketAggParam, value: any, agg: IBucketAggConfig) { + serialize(this: BucketAggParam, value: any, agg: IBucketAggConfig) { if (this.shouldShow && !this.shouldShow(agg)) return; if (!value || isString(value)) return value; else return value.pattern; @@ -49,4 +50,4 @@ export const migrateIncludeExcludeFormat = { output.params[this.name] = value; } }, -}; +} as Partial>; diff --git a/src/legacy/ui/public/agg_types/buckets/range.ts b/src/legacy/ui/public/agg_types/buckets/range.ts index 89529442b24a67..24757a607e0052 100644 --- a/src/legacy/ui/public/agg_types/buckets/range.ts +++ b/src/legacy/ui/public/agg_types/buckets/range.ts @@ -18,7 +18,6 @@ */ import { i18n } from '@kbn/i18n'; -import { IBucketAggConfig } from './_bucket_agg_type'; import { BucketAggType } from './_bucket_agg_type'; import { FieldFormat, KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; import { RangeKey } from './range_key'; @@ -102,7 +101,7 @@ export const rangeBucketAgg = new BucketAggType({ { from: 1000, to: 2000 }, ], editorComponent: RangesEditor, - write(aggConfig: IBucketAggConfig, output: Record) { + write(aggConfig, output) { output.params.ranges = aggConfig.params.ranges; output.params.keyed = true; }, diff --git a/src/legacy/ui/public/agg_types/buckets/significant_terms.test.ts b/src/legacy/ui/public/agg_types/buckets/significant_terms.test.ts index 454f1bf70a790b..8db9226e41eec7 100644 --- a/src/legacy/ui/public/agg_types/buckets/significant_terms.test.ts +++ b/src/legacy/ui/public/agg_types/buckets/significant_terms.test.ts @@ -20,6 +20,7 @@ import { AggConfigs } from '../index'; import { BUCKET_TYPES } from './bucket_agg_types'; import { significantTermsBucketAgg } from './significant_terms'; +import { IBucketAggConfig } from './_bucket_agg_type'; jest.mock('ui/new_platform'); @@ -71,7 +72,7 @@ describe('Significant Terms Agg', () => { name: 'FIELD', }, }); - const label = significantTermsBucketAgg.makeLabel(aggConfigs.aggs[0]); + const label = significantTermsBucketAgg.makeLabel(aggConfigs.aggs[0] as IBucketAggConfig); expect(label).toBe('Top SIZE unusual terms in FIELD'); }); diff --git a/src/legacy/ui/public/agg_types/buckets/significant_terms.ts b/src/legacy/ui/public/agg_types/buckets/significant_terms.ts index 65c73e5f9b7dd3..128fd9e83e6fd9 100644 --- a/src/legacy/ui/public/agg_types/buckets/significant_terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/significant_terms.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { SizeParamEditor } from '../../vis/editors/default/controls/size'; -import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; +import { BucketAggType } from './_bucket_agg_type'; import { createFilterTerms } from './create_filter/terms'; import { isStringType, migrateIncludeExcludeFormat } from './migrate_include_exclude_format'; import { BUCKET_TYPES } from './bucket_agg_types'; @@ -63,7 +63,7 @@ export const significantTermsBucketAgg = new BucketAggType({ advanced: true, shouldShow: isStringType, ...migrateIncludeExcludeFormat, - } as BucketAggParam, + }, { name: 'include', displayName: i18n.translate('common.ui.aggTypes.buckets.significantTerms.includeLabel', { @@ -73,6 +73,6 @@ export const significantTermsBucketAgg = new BucketAggType({ advanced: true, shouldShow: isStringType, ...migrateIncludeExcludeFormat, - } as BucketAggParam, + }, ], }); diff --git a/src/legacy/ui/public/agg_types/buckets/terms.ts b/src/legacy/ui/public/agg_types/buckets/terms.ts index 6ce0b9ce38ad34..e38f7ca4cc038e 100644 --- a/src/legacy/ui/public/agg_types/buckets/terms.ts +++ b/src/legacy/ui/public/agg_types/buckets/terms.ts @@ -21,9 +21,8 @@ import chrome from 'ui/chrome'; import { noop } from 'lodash'; import { i18n } from '@kbn/i18n'; import { SearchSource, getRequestInspectorStats, getResponseInspectorStats } from '../../courier'; -import { BucketAggType, BucketAggParam } from './_bucket_agg_type'; +import { BucketAggType } from './_bucket_agg_type'; import { BUCKET_TYPES } from './bucket_agg_types'; -import { AggConfigOptions } from '../agg_config'; import { IBucketAggConfig } from './_bucket_agg_type'; import { createFilterTerms } from './create_filter/terms'; import { wrapWithInlineComp } from './inline_comp_wrapper'; @@ -158,18 +157,21 @@ export const termsBucketAgg = new BucketAggType({ type: 'agg', default: null, editorComponent: OrderAggParamEditor, - makeAgg(termsAgg: IBucketAggConfig, state: AggConfigOptions) { + makeAgg(termsAgg, state) { state = state || {}; state.schema = orderAggSchema; - const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); + const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { + addToAggConfigs: false, + }); orderAgg.id = termsAgg.id + '-orderAgg'; + return orderAgg; }, - write(agg: IBucketAggConfig, output: Record, aggs: AggConfigs) { + write(agg, output, aggs) { const dir = agg.params.order.value; const order: Record = (output.params.order = {}); - let orderAgg = agg.params.orderAgg || aggs.getResponseAggById(agg.params.orderBy); + let orderAgg = agg.params.orderAgg || aggs!.getResponseAggById(agg.params.orderBy); // TODO: This works around an Elasticsearch bug the always casts terms agg scripts to strings // thus causing issues with filtering. This probably causes other issues since float might not @@ -194,7 +196,8 @@ export const termsBucketAgg = new BucketAggType({ } const orderAggId = orderAgg.id; - if (orderAgg.parentId) { + + if (orderAgg.parentId && aggs) { orderAgg = aggs.byId(orderAgg.parentId); } @@ -243,9 +246,9 @@ export const termsBucketAgg = new BucketAggType({ displayName: i18n.translate('common.ui.aggTypes.otherBucket.labelForOtherBucketLabel', { defaultMessage: 'Label for other bucket', }), - shouldShow: (agg: IBucketAggConfig) => agg.getParam('otherBucket'), + shouldShow: agg => agg.getParam('otherBucket'), write: noop, - } as BucketAggParam, + }, { name: 'missingBucket', default: false, @@ -263,7 +266,7 @@ export const termsBucketAgg = new BucketAggType({ displayName: i18n.translate('common.ui.aggTypes.otherBucket.labelForMissingValuesLabel', { defaultMessage: 'Label for missing values', }), - shouldShow: (agg: IBucketAggConfig) => agg.getParam('missingBucket'), + shouldShow: agg => agg.getParam('missingBucket'), write: noop, }, { @@ -286,5 +289,5 @@ export const termsBucketAgg = new BucketAggType({ shouldShow: isStringType, ...migrateIncludeExcludeFormat, }, - ] as BucketAggParam[], + ], }); diff --git a/src/legacy/ui/public/agg_types/metrics/lib/nested_agg_helpers.ts b/src/legacy/ui/public/agg_types/metrics/lib/nested_agg_helpers.ts index dac36ac8a89ca7..a7bfb7b82b97f6 100644 --- a/src/legacy/ui/public/agg_types/metrics/lib/nested_agg_helpers.ts +++ b/src/legacy/ui/public/agg_types/metrics/lib/nested_agg_helpers.ts @@ -44,7 +44,7 @@ export const forwardModifyAggConfigOnSearchRequestStart = (paramName: string) => const nestedAggConfig = aggConfig.getParam(paramName); if (nestedAggConfig && nestedAggConfig.type && nestedAggConfig.type.params) { - nestedAggConfig.type.params.forEach((param: MetricAggParam) => { + nestedAggConfig.type.params.forEach((param: MetricAggParam) => { // Check if this parameter of the nested aggConfig has a modifyAggConfigOnSearchRequestStart // function, that needs to be called. if (param.modifyAggConfigOnSearchRequestStart) { diff --git a/src/legacy/ui/public/utils/__tests__/ordinal_suffix.js b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts similarity index 76% rename from src/legacy/ui/public/utils/__tests__/ordinal_suffix.js rename to src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts index dae12d41cfb5b9..18ee6b4de32044 100644 --- a/src/legacy/ui/public/utils/__tests__/ordinal_suffix.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.test.ts @@ -17,11 +17,10 @@ * under the License. */ -import _ from 'lodash'; -import { ordinalSuffix } from '../ordinal_suffix'; -import expect from '@kbn/expect'; +import { forOwn } from 'lodash'; +import { ordinalSuffix } from './ordinal_suffix'; -describe('ordinal suffix util', function () { +describe('ordinal suffix util', () => { const checks = { 1: 'st', 2: 'nd', @@ -52,19 +51,19 @@ describe('ordinal suffix util', function () { 27: 'th', 28: 'th', 29: 'th', - 30: 'th' + 30: 'th', }; - _.forOwn(checks, function (expected, num) { + forOwn(checks, (expected, num: any) => { const int = parseInt(num, 10); const float = int + Math.random(); - it('knowns ' + int, function () { - expect(ordinalSuffix(num)).to.be(num + '' + expected); + it('knowns ' + int, () => { + expect(ordinalSuffix(num)).toBe(num + '' + expected); }); - it('knows ' + float, function () { - expect(ordinalSuffix(num)).to.be(num + '' + expected); + it('knows ' + float, () => { + expect(ordinalSuffix(num)).toBe(num + '' + expected); }); }); }); diff --git a/src/legacy/ui/public/utils/ordinal_suffix.js b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts similarity index 93% rename from src/legacy/ui/public/utils/ordinal_suffix.js rename to src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts index 64fb29c8ae534e..21903995ebb2ff 100644 --- a/src/legacy/ui/public/utils/ordinal_suffix.js +++ b/src/legacy/ui/public/agg_types/metrics/lib/ordinal_suffix.ts @@ -18,11 +18,11 @@ */ // adopted from http://stackoverflow.com/questions/3109978/php-display-number-with-ordinal-suffix -export function ordinalSuffix(num) { +export function ordinalSuffix(num: any): string { return num + '' + suffix(num); } -function suffix(num) { +function suffix(num: any): string { const int = Math.floor(parseFloat(num)); const hunth = int % 100; diff --git a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.ts b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.ts index e4726c62428cba..d177a62649d134 100644 --- a/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/legacy/ui/public/agg_types/metrics/lib/parent_pipeline_agg_helper.ts @@ -22,7 +22,7 @@ import { noop } from 'lodash'; import { MetricAggParamEditor } from '../../../vis/editors/default/controls/metric_agg'; import { SubAggParamEditor } from '../../../vis/editors/default/controls/sub_agg'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; -import { IMetricAggConfig } from '../metric_agg_type'; +import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; import { parentPipelineAggWriter } from './parent_pipeline_agg_writer'; // @ts-ignore @@ -74,7 +74,7 @@ export const parentPipelineAggHelper = { name: 'customMetric', editorComponent: SubAggParamEditor, type: 'agg', - makeAgg(termsAgg: IMetricAggConfig, state: any) { + makeAgg(termsAgg, state: any) { state = state || { type: 'count' }; state.schema = metricAggSchema; @@ -93,7 +93,7 @@ export const parentPipelineAggHelper = { name: 'buckets_path', write: noop, }, - ]; + ] as Array>; }, getFormat(agg: IMetricAggConfig) { diff --git a/src/legacy/ui/public/agg_types/metrics/lib/sibling_pipeline_agg_helper.ts b/src/legacy/ui/public/agg_types/metrics/lib/sibling_pipeline_agg_helper.ts index 1df771727f6cc8..e75ebf366a27e6 100644 --- a/src/legacy/ui/public/agg_types/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/legacy/ui/public/agg_types/metrics/lib/sibling_pipeline_agg_helper.ts @@ -22,7 +22,7 @@ import { siblingPipelineAggWriter } from './sibling_pipeline_agg_writer'; import { SubMetricParamEditor } from '../../../vis/editors/default/controls/sub_metric'; import { forwardModifyAggConfigOnSearchRequestStart } from './nested_agg_helpers'; -import { IMetricAggConfig } from '../metric_agg_type'; +import { IMetricAggConfig, MetricAggParam } from '../metric_agg_type'; // @ts-ignore import { Schemas } from '../../../vis/editors/default/schemas'; @@ -110,7 +110,7 @@ const siblingPipelineAggHelper = { ), write: siblingPipelineAggWriter, }, - ]; + ] as Array>; }, getFormat(agg: IMetricAggConfig) { diff --git a/src/legacy/ui/public/agg_types/metrics/median.ts b/src/legacy/ui/public/agg_types/metrics/median.ts index 8797bed5105c52..5792d4a7c2ba3b 100644 --- a/src/legacy/ui/public/agg_types/metrics/median.ts +++ b/src/legacy/ui/public/agg_types/metrics/median.ts @@ -17,7 +17,7 @@ * under the License. */ import { i18n } from '@kbn/i18n'; -import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; // @ts-ignore @@ -49,7 +49,7 @@ export const medianMetricAgg = new MetricAggType({ default: [50], }, { - write(agg: IMetricAggConfig, output: Record) { + write(agg, output) { output.params.keyed = false; }, }, diff --git a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts index c24dda180ea94d..29499c5be84b84 100644 --- a/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts +++ b/src/legacy/ui/public/agg_types/metrics/metric_agg_type.ts @@ -25,24 +25,28 @@ import { AggConfig } from '../agg_config'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; -export type IMetricAggConfig = AggConfig; - -export interface MetricAggTypeConfig - extends AggTypeConfig { - isScalable?: () => boolean; - subtype?: string; +export interface IMetricAggConfig extends AggConfig { + type: InstanceType; } -export interface MetricAggParam extends AggParamType { +export interface MetricAggParam + extends AggParamType { filterFieldTypes?: KBN_FIELD_TYPES | KBN_FIELD_TYPES[] | '*'; onlyAggregatable?: boolean; } const metricType = 'metrics'; -export class MetricAggType< - TMetricAggConfig extends IMetricAggConfig = IMetricAggConfig -> extends AggType { +interface MetricAggTypeConfig + extends AggTypeConfig> { + isScalable?: () => boolean; + subtype?: string; +} + +export class MetricAggType extends AggType< + TMetricAggConfig, + MetricAggParam +> { subtype: string; isScalable: () => boolean; type = metricType; diff --git a/src/legacy/ui/public/agg_types/metrics/parent_pipeline.test.ts b/src/legacy/ui/public/agg_types/metrics/parent_pipeline.test.ts index 7c7a2a68cd7c55..0adf41a0420a0a 100644 --- a/src/legacy/ui/public/agg_types/metrics/parent_pipeline.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/parent_pipeline.test.ts @@ -111,7 +111,7 @@ describe('parent pipeline aggs', function() { // Grab the aggConfig off the vis (we don't actually use the vis for anything else) metricAgg = metric.provider; - aggConfig = aggConfigs.aggs[1]; + aggConfig = aggConfigs.aggs[1] as IMetricAggConfig; aggDsl = aggConfig.toDsl(aggConfigs); }; diff --git a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts index ead5122278b5a2..1a1d5bf04309fd 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentile_ranks.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { npStart } from 'ui/new_platform'; import { PercentileRanksEditor } from '../../vis/editors/default/controls/percentile_ranks'; -import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { MetricAggType } from './metric_agg_type'; import { getResponseAggConfigClass, IResponseAggConfig } from './lib/get_response_agg_config_class'; import { getPercentileValue } from './percentiles_get_value'; @@ -72,7 +72,7 @@ export const percentileRanksMetricAgg = new MetricAggType) { + write(agg, output) { output.params.keyed = false; }, }, diff --git a/src/legacy/ui/public/agg_types/metrics/percentiles.ts b/src/legacy/ui/public/agg_types/metrics/percentiles.ts index 1a3606d6779516..9b8205425b4a46 100644 --- a/src/legacy/ui/public/agg_types/metrics/percentiles.ts +++ b/src/legacy/ui/public/agg_types/metrics/percentiles.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { IMetricAggConfig, MetricAggType } from './metric_agg_type'; +import { MetricAggType } from './metric_agg_type'; import { METRIC_TYPES } from './metric_agg_types'; import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public'; @@ -28,7 +28,7 @@ import { getPercentileValue } from './percentiles_get_value'; import { PercentilesEditor } from '../../vis/editors/default/controls/percentiles'; // @ts-ignore -import { ordinalSuffix } from '../../utils/ordinal_suffix'; +import { ordinalSuffix } from './lib/ordinal_suffix'; export type IPercentileAggConfig = IResponseAggConfig; @@ -67,7 +67,7 @@ export const percentilesMetricAgg = new MetricAggType({ default: [1, 5, 25, 50, 75, 95, 99], }, { - write(agg: IMetricAggConfig, output: Record) { + write(agg, output) { output.params.keyed = false; }, }, diff --git a/src/legacy/ui/public/agg_types/metrics/sibling_pipeline.test.ts b/src/legacy/ui/public/agg_types/metrics/sibling_pipeline.test.ts index e038936de07d20..60165790da5455 100644 --- a/src/legacy/ui/public/agg_types/metrics/sibling_pipeline.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/sibling_pipeline.test.ts @@ -107,7 +107,7 @@ describe('sibling pipeline aggs', () => { // Grab the aggConfig off the vis (we don't actually use the vis for anything else) metricAgg = metric.provider; - aggConfig = aggConfigs.aggs[1]; + aggConfig = aggConfigs.aggs[1] as IMetricAggConfig; aggDsl = aggConfig.toDsl(aggConfigs); }; diff --git a/src/legacy/ui/public/agg_types/metrics/top_hit.test.ts b/src/legacy/ui/public/agg_types/metrics/top_hit.test.ts index 051174c388c1b8..3e861c052d3671 100644 --- a/src/legacy/ui/public/agg_types/metrics/top_hit.test.ts +++ b/src/legacy/ui/public/agg_types/metrics/top_hit.test.ts @@ -85,7 +85,7 @@ describe('Top hit metric', () => { ); // Grab the aggConfig off the vis (we don't actually use the vis for anything else) - aggConfig = aggConfigs.aggs[0]; + aggConfig = aggConfigs.aggs[0] as IMetricAggConfig; aggDsl = aggConfig.toDsl(aggConfigs); }; diff --git a/src/legacy/ui/public/agg_types/metrics/top_hit.ts b/src/legacy/ui/public/agg_types/metrics/top_hit.ts index 49c4a7951fab91..4b07c997f11e0a 100644 --- a/src/legacy/ui/public/agg_types/metrics/top_hit.ts +++ b/src/legacy/ui/public/agg_types/metrics/top_hit.ts @@ -38,7 +38,7 @@ const isNumericFieldSelected = (agg: IMetricAggConfig) => { return field && field.type && field.type === KBN_FIELD_TYPES.NUMBER; }; -aggTypeFieldFilters.addFilter((field, aggConfig: IMetricAggConfig) => { +aggTypeFieldFilters.addFilter((field, aggConfig) => { if ( aggConfig.type.name !== METRIC_TYPES.TOP_HITS || _.get(aggConfig.schema, 'aggSettings.top_hits.allowStrings', false) @@ -82,7 +82,7 @@ export const topHitMetricAgg = new MetricAggType({ editorComponent: TopFieldParamEditor, onlyAggregatable: false, filterFieldTypes: '*', - write(agg: IMetricAggConfig, output: Record) { + write(agg, output) { const field = agg.getParam('field'); output.params = {}; @@ -196,7 +196,7 @@ export const topHitMetricAgg = new MetricAggType({ value: 'asc', }, ], - write(agg: IMetricAggConfig, output: Record) { + write(agg, output) { const sortField = agg.params.sortField; const sortOrder = agg.params.sortOrder; diff --git a/src/legacy/ui/public/agg_types/param_types/agg.ts b/src/legacy/ui/public/agg_types/param_types/agg.ts index 71b2d41bdb773f..0a83805c8c44cc 100644 --- a/src/legacy/ui/public/agg_types/param_types/agg.ts +++ b/src/legacy/ui/public/agg_types/param_types/agg.ts @@ -20,26 +20,28 @@ import { AggConfig } from '../agg_config'; import { BaseParamType } from './base'; -export class AggParamType extends BaseParamType { - makeAgg: (agg: AggConfig, state?: any) => AggConfig; +export class AggParamType extends BaseParamType< + TAggConfig +> { + makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; constructor(config: Record) { super(config); if (!config.write) { - this.write = (aggConfig: AggConfig, output: Record) => { + this.write = (aggConfig: TAggConfig, output: Record) => { if (aggConfig.params[this.name] && aggConfig.params[this.name].length) { output.params[this.name] = aggConfig.params[this.name]; } }; } if (!config.serialize) { - this.serialize = (agg: AggConfig) => { + this.serialize = (agg: TAggConfig) => { return agg.toJSON(); }; } if (!config.deserialize) { - this.deserialize = (state: unknown, agg?: AggConfig): AggConfig => { + this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => { if (!agg) { throw new Error('aggConfig was not provided to AggParamType deserialize function'); } diff --git a/src/legacy/ui/public/agg_types/param_types/base.ts b/src/legacy/ui/public/agg_types/param_types/base.ts index 61ef73fb62e8a3..bc8fd30e6324e3 100644 --- a/src/legacy/ui/public/agg_types/param_types/base.ts +++ b/src/legacy/ui/public/agg_types/param_types/base.ts @@ -17,12 +17,11 @@ * under the License. */ -import { AggParam } from '../'; import { AggConfigs } from '../agg_configs'; import { AggConfig } from '../../vis'; import { SearchSourceContract, FetchOptions } from '../../courier/types'; -export class BaseParamType implements AggParam { +export class BaseParamType { name: string; type: string; displayName: string; @@ -31,18 +30,18 @@ export class BaseParamType implements AggParam { editorComponent: any = null; default: any; write: ( - aggConfig: AggConfig, + aggConfig: TAggConfig, output: Record, aggConfigs?: AggConfigs, locals?: Record ) => void; - serialize: (value: any, aggConfig?: AggConfig) => any; - deserialize: (value: any, aggConfig?: AggConfig) => any; + serialize: (value: any, aggConfig?: TAggConfig) => any; + deserialize: (value: any, aggConfig?: TAggConfig) => any; options: any[]; valueType?: any; - onChange?(agg: AggConfig): void; - shouldShow?(agg: AggConfig): boolean; + onChange?(agg: TAggConfig): void; + shouldShow?(agg: TAggConfig): boolean; /** * A function that will be called before an aggConfig is serialized and sent to ES. @@ -54,7 +53,7 @@ export class BaseParamType implements AggParam { * @returns {Promise|undefined} */ modifyAggConfigOnSearchRequestStart: ( - aggConfig: AggConfig, + aggConfig: TAggConfig, searchSource?: SearchSourceContract, options?: FetchOptions ) => void; @@ -70,7 +69,7 @@ export class BaseParamType implements AggParam { this.default = config.default; this.editorComponent = config.editorComponent; - const defaultWrite = (aggConfig: AggConfig, output: Record) => { + const defaultWrite = (aggConfig: TAggConfig, output: Record) => { if (aggConfig.params[this.name]) { output.params[this.name] = aggConfig.params[this.name] || this.default; } diff --git a/src/legacy/ui/public/field_editor/field_editor.js b/src/legacy/ui/public/field_editor/field_editor.js index f3c5990caae64a..130d969e5cd025 100644 --- a/src/legacy/ui/public/field_editor/field_editor.js +++ b/src/legacy/ui/public/field_editor/field_editor.js @@ -689,7 +689,6 @@ export class FieldEditor extends PureComponent { } saveField = async () => { - const fieldFormat = this.state.field.format; const field = this.state.field.toActualField(); const { indexPattern } = this.props; const { fieldFormatId } = this.state; @@ -727,7 +726,7 @@ export class FieldEditor extends PureComponent { if (!fieldFormatId) { indexPattern.fieldFormatMap[field.name] = undefined; } else { - indexPattern.fieldFormatMap[field.name] = fieldFormat; + indexPattern.fieldFormatMap[field.name] = field.format; } return indexPattern.save() diff --git a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts index 5c7f7be0603741..006497435aec89 100644 --- a/src/legacy/ui/public/new_platform/__mocks__/helpers.ts +++ b/src/legacy/ui/public/new_platform/__mocks__/helpers.ts @@ -24,6 +24,7 @@ import { embeddablePluginMock } from '../../../../../plugins/embeddable/public/m import { expressionsPluginMock } from '../../../../../plugins/expressions/public/mocks'; import { inspectorPluginMock } from '../../../../../plugins/inspector/public/mocks'; import { uiActionsPluginMock } from '../../../../../plugins/ui_actions/public/mocks'; +import { usageCollectionPluginMock } from '../../../../../plugins/usage_collection/public/mocks'; /* eslint-enable @kbn/eslint/no-restricted-paths */ export const pluginsMock = { @@ -33,6 +34,7 @@ export const pluginsMock = { inspector: inspectorPluginMock.createSetupContract(), expressions: expressionsPluginMock.createSetupContract(), uiActions: uiActionsPluginMock.createSetupContract(), + usageCollection: usageCollectionPluginMock.createSetupContract(), }), createStart: () => ({ data: dataPluginMock.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 f8850d1691cddf..dd91a538e339f4 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 @@ -19,6 +19,7 @@ import sinon from 'sinon'; import { getFieldFormatsRegistry } from '../../../../test_utils/public/stub_field_formats'; +import { METRIC_TYPE } from '@kbn/analytics'; const mockObservable = () => { return { @@ -50,6 +51,11 @@ export const npSetup = { uiSettings: mockUiSettings, }, plugins: { + usageCollection: { + allowTrackUserAgent: sinon.fake(), + reportUiStats: sinon.fake(), + METRIC_TYPE, + }, embeddable: { registerEmbeddableFactory: sinon.fake(), }, @@ -153,8 +159,10 @@ export const npStart = { getProvider: sinon.fake(), }, getSuggestions: sinon.fake(), + indexPatterns: sinon.fake(), ui: { IndexPatternSelect: mockComponent, + SearchBar: mockComponent, }, query: { filterManager: { diff --git a/src/legacy/ui/public/new_platform/new_platform.test.ts b/src/legacy/ui/public/new_platform/new_platform.test.ts index e5d5cd0a877764..cd1af311d4eff0 100644 --- a/src/legacy/ui/public/new_platform/new_platform.test.ts +++ b/src/legacy/ui/public/new_platform/new_platform.test.ts @@ -58,6 +58,26 @@ describe('ui/new_platform', () => { const scopeMock = { $on: jest.fn() }; const elementMock = [document.createElement('div')]; + controller(scopeMock, elementMock); + expect(mountMock).toHaveBeenCalledWith({ + element: elementMock[0], + appBasePath: '/test/base/path/app/test', + }); + }); + + test('controller calls deprecated context app.mount when invoked', () => { + const unmountMock = jest.fn(); + // Two arguments changes how this is called. + const mountMock = jest.fn((context, params) => unmountMock); + legacyAppRegister({ + id: 'test', + title: 'Test', + mount: mountMock, + }); + const controller = setRootControllerMock.mock.calls[0][1]; + const scopeMock = { $on: jest.fn() }; + const elementMock = [document.createElement('div')]; + controller(scopeMock, elementMock); expect(mountMock).toHaveBeenCalledWith(expect.any(Object), { element: elementMock[0], diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index c0b2d6d9132578..fd6b417d504aad 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -20,7 +20,7 @@ import { IScope } from 'angular'; import { IUiActionsStart, IUiActionsSetup } from 'src/plugins/ui_actions/public'; import { IEmbeddableStart, IEmbeddableSetup } from 'src/plugins/embeddable/public'; -import { LegacyCoreSetup, LegacyCoreStart, App } from '../../../../core/public'; +import { LegacyCoreSetup, LegacyCoreStart, App, AppMountDeprecated } from '../../../../core/public'; import { Plugin as DataPlugin } from '../../../../plugins/data/public'; import { Plugin as ExpressionsPlugin } from '../../../../plugins/expressions/public'; import { @@ -32,6 +32,7 @@ import { DevToolsSetup, DevToolsStart } from '../../../../plugins/dev_tools/publ import { KibanaLegacySetup, KibanaLegacyStart } from '../../../../plugins/kibana_legacy/public'; import { HomePublicPluginSetup, HomePublicPluginStart } from '../../../../plugins/home/public'; import { SharePluginSetup, SharePluginStart } from '../../../../plugins/share/public'; +import { UsageCollectionSetup } from '../../../../plugins/usage_collection/public'; export interface PluginsSetup { data: ReturnType; @@ -43,6 +44,7 @@ export interface PluginsSetup { dev_tools: DevToolsSetup; kibana_legacy: KibanaLegacySetup; share: SharePluginSetup; + usageCollection: UsageCollectionSetup; } export interface PluginsStart { @@ -111,13 +113,18 @@ export const legacyAppRegister = (app: App) => { // Root controller cannot return a Promise so use an internal async function and call it immediately (async () => { - const unmount = await app.mount( - { core: npStart.core }, - { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) } - ); + const params = { element, appBasePath: npSetup.core.http.basePath.prepend(`/app/${app.id}`) }; + const unmount = isAppMountDeprecated(app.mount) + ? await app.mount({ core: npStart.core }, params) + : await app.mount(params); $scope.$on('$destroy', () => { unmount(); }); })(); }); }; + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js index 766ed44a4c0fee..ccd2ba6a6c5104 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js +++ b/src/legacy/ui/public/saved_objects/__tests__/find_object_by_title.js @@ -19,7 +19,7 @@ import sinon from 'sinon'; import expect from '@kbn/expect'; -import { findObjectByTitle } from '../find_object_by_title'; +import { findObjectByTitle } from '../helpers/find_object_by_title'; import { SimpleSavedObject } from '../../../../../core/public'; describe('findObjectByTitle', () => { diff --git a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js index 56124a047ba6d0..720fc63fb89e78 100644 --- a/src/legacy/ui/public/saved_objects/__tests__/saved_object.js +++ b/src/legacy/ui/public/saved_objects/__tests__/saved_object.js @@ -22,9 +22,9 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import Bluebird from 'bluebird'; -import { SavedObjectProvider } from '../saved_object'; +import { createSavedObjectClass } from '../saved_object'; import StubIndexPattern from 'test_utils/stub_index_pattern'; -import { SavedObjectsClientProvider } from '../saved_objects_client_provider'; +import { npStart } from 'ui/new_platform'; import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; import { mockUiSettings } from '../../new_platform/new_platform.karma_mock'; @@ -82,6 +82,10 @@ describe('Saved Object', function () { return savedObject.init(); } + function restoreIfWrapped(obj, fName) { + obj[fName].restore && obj[fName].restore(); + } + const mock409FetchError = { res: { status: 409 } }; @@ -96,13 +100,21 @@ describe('Saved Object', function () { }) ); - beforeEach(ngMock.inject(function (es, Private, $window) { - SavedObject = Private(SavedObjectProvider); + beforeEach(ngMock.inject(function (es, $window) { + savedObjectsClientStub = npStart.core.savedObjects.client; + SavedObject = createSavedObjectClass({ savedObjectsClient: savedObjectsClientStub }); esDataStub = es; - savedObjectsClientStub = Private(SavedObjectsClientProvider); window = $window; })); + afterEach(ngMock.inject(function () { + restoreIfWrapped(savedObjectsClientStub, 'create'); + restoreIfWrapped(savedObjectsClientStub, 'get'); + restoreIfWrapped(savedObjectsClientStub, 'update'); + restoreIfWrapped(savedObjectsClientStub, 'find'); + restoreIfWrapped(savedObjectsClientStub, 'bulkGet'); + })); + describe('save', function () { describe('with confirmOverwrite', function () { function stubConfirmOverwrite() { @@ -110,66 +122,6 @@ describe('Saved Object', function () { sinon.stub(esDataStub, 'create').returns(Bluebird.reject(mock409FetchError)); } - describe('when true', function () { - it('requests confirmation and updates on yes response', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - const createStub = sinon.stub(savedObjectsClientStub, 'create'); - createStub.onFirstCall().returns(Bluebird.reject(mock409FetchError)); - createStub.onSecondCall().returns(Bluebird.resolve({ id: 'myId' })); - - stubConfirmOverwrite(); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(window.confirm.called).to.be(true); - expect(savedObject.id).to.be('myId'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('new title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('does not update on no response', function () { - stubESResponse(getMockedDocResponse('HI')); - return createInitializedSavedObject({ type: 'dashboard', id: 'HI' }).then(savedObject => { - window.confirm = sinon.stub().returns(false); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - savedObject.lastSavedTitle = 'original title'; - savedObject.title = 'new title'; - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(savedObject.id).to.be('HI'); - expect(savedObject.isSaving).to.be(false); - expect(savedObject.lastSavedTitle).to.be('original title'); - expect(savedObject.title).to.be('new title'); - }); - }); - }); - - it('handles create failures', function () { - stubESResponse(getMockedDocResponse('myId')); - return createInitializedSavedObject({ type: 'dashboard', id: 'myId' }).then(savedObject => { - stubConfirmOverwrite(); - - sinon.stub(savedObjectsClientStub, 'create').returns(Bluebird.reject(mock409FetchError)); - - return savedObject.save({ confirmOverwrite: true }) - .then(() => { - expect(true).to.be(false); // Force failure, the save should not succeed. - }) - .catch(() => { - expect(window.confirm.called).to.be(true); - }); - }); - }); - }); - it('when false does not request overwrite', function () { const mockDocResponse = getMockedDocResponse('myId'); stubESResponse(mockDocResponse); @@ -691,18 +643,6 @@ describe('Saved Object', function () { }); }); - it('init is called', function () { - const initCallback = sinon.spy(); - const config = { - type: 'dashboard', - init: initCallback - }; - - return createInitializedSavedObject(config).then(() => { - expect(initCallback.called).to.be(true); - }); - }); - describe('searchSource', function () { it('when true, creates index', function () { const indexPatternId = 'testIndexPattern'; diff --git a/src/legacy/server/url_shortening/routes/shorten_url.js b/src/legacy/ui/public/saved_objects/constants.ts similarity index 56% rename from src/legacy/server/url_shortening/routes/shorten_url.js rename to src/legacy/ui/public/saved_objects/constants.ts index 0203e9373384a8..e1684aa1d19d57 100644 --- a/src/legacy/server/url_shortening/routes/shorten_url.js +++ b/src/legacy/ui/public/saved_objects/constants.ts @@ -16,20 +16,25 @@ * specific language governing permissions and limitations * under the License. */ +import { i18n } from '@kbn/i18n'; -import { handleShortUrlError } from './lib/short_url_error'; -import { shortUrlAssertValid } from './lib/short_url_assert_valid'; - -export const createShortenUrlRoute = ({ shortUrlLookup }) => ({ - method: 'POST', - path: '/api/shorten_url', - handler: async function (request) { - try { - shortUrlAssertValid(request.payload.url); - const urlId = await shortUrlLookup.generateUrlId(request.payload.url, request); - return { urlId }; - } catch (err) { - throw handleShortUrlError(err); - } +/** + * An error message to be used when the user rejects a confirm overwrite. + * @type {string} + */ +export const OVERWRITE_REJECTED = i18n.translate( + 'common.ui.savedObjects.overwriteRejectedDescription', + { + defaultMessage: 'Overwrite confirmation was rejected', + } +); +/** + * An error message to be used when the user rejects a confirm save with duplicate title. + * @type {string} + */ +export const SAVE_DUPLICATE_REJECTED = i18n.translate( + 'common.ui.savedObjects.saveDuplicateRejectedDescription', + { + defaultMessage: 'Save with duplicate title confirmation was rejected', } -}); +); diff --git a/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.ts new file mode 100644 index 00000000000000..77f504d1080767 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/apply_es_resp.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 _ from 'lodash'; +import { EsResponse, SavedObject, SavedObjectConfig } from 'ui/saved_objects/types'; +import { parseSearchSource } from 'ui/saved_objects/helpers/parse_search_source'; +import { expandShorthand, SavedObjectNotFound } from '../../../../../plugins/kibana_utils/public'; +import { IndexPattern } from '../../../../core_plugins/data/public'; + +/** + * A given response of and ElasticSearch containing a plain saved object is applied to the given + * savedObject + */ +export async function applyESResp( + resp: EsResponse, + savedObject: SavedObject, + config: SavedObjectConfig +) { + const mapping = expandShorthand(config.mapping); + const esType = config.type || ''; + savedObject._source = _.cloneDeep(resp._source); + const injectReferences = config.injectReferences; + const hydrateIndexPattern = savedObject.hydrateIndexPattern!; + if (typeof resp.found === 'boolean' && !resp.found) { + throw new SavedObjectNotFound(esType, savedObject.id || ''); + } + + const meta = resp._source.kibanaSavedObjectMeta || {}; + delete resp._source.kibanaSavedObjectMeta; + + if (!config.indexPattern && savedObject._source.indexPattern) { + config.indexPattern = savedObject._source.indexPattern as IndexPattern; + delete savedObject._source.indexPattern; + } + + // assign the defaults to the response + _.defaults(savedObject._source, savedObject.defaults); + + // transform the source using _deserializers + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (fieldMapping._deserialize && typeof fieldName === 'string') { + savedObject._source[fieldName] = fieldMapping._deserialize( + savedObject._source[fieldName] as string + ); + } + }); + + // Give obj all of the values in _source.fields + _.assign(savedObject, savedObject._source); + savedObject.lastSavedTitle = savedObject.title; + + try { + await parseSearchSource(savedObject, esType, meta.searchSourceJSON, resp.references); + await hydrateIndexPattern(); + if (injectReferences && resp.references && resp.references.length > 0) { + injectReferences(savedObject, resp.references); + } + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + throw e; + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.ts new file mode 100644 index 00000000000000..a436f70f31ffe1 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/build_saved_object.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 _ from 'lodash'; +import { SearchSource } from 'ui/courier'; +import { hydrateIndexPattern } from './hydrate_index_pattern'; +import { intializeSavedObject } from './initialize_saved_object'; +import { serializeSavedObject } from './serialize_saved_object'; + +import { + EsResponse, + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { applyESResp } from './apply_es_resp'; +import { saveSavedObject } from './save_saved_object'; + +export function buildSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig = {}, + services: SavedObjectKibanaServices +) { + const { indexPatterns, savedObjectsClient } = services; + // type name for this object, used as the ES-type + const esType = config.type || ''; + + savedObject.getDisplayName = () => esType; + + // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or + // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. + savedObject.getEsType = () => esType; + + /** + * Flips to true during a save operation, and back to false once the save operation + * completes. + * @type {boolean} + */ + savedObject.isSaving = false; + savedObject.defaults = config.defaults || {}; + // optional search source which this object configures + savedObject.searchSource = config.searchSource ? new SearchSource() : undefined; + // the id of the document + savedObject.id = config.id || void 0; + // the migration version of the document, should only be set on imports + savedObject.migrationVersion = config.migrationVersion; + // Whether to create a copy when the object is saved. This should eventually go away + // in favor of a better rename/save flow. + savedObject.copyOnSave = false; + + /** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ + savedObject.hydrateIndexPattern = (id?: string) => + hydrateIndexPattern(id || '', savedObject, indexPatterns, config); + /** + * Asynchronously initialize this object - will only run + * once even if called multiple times. + * + * @return {Promise} + * @resolved {SavedObject} + */ + savedObject.init = _.once(() => intializeSavedObject(savedObject, savedObjectsClient, config)); + + savedObject.applyESResp = (resp: EsResponse) => applyESResp(resp, savedObject, config); + + /** + * Serialize this object + * @return {Object} + */ + savedObject._serialize = () => serializeSavedObject(savedObject, config); + + /** + * Returns true if the object's original title has been changed. New objects return false. + * @return {boolean} + */ + savedObject.isTitleChanged = () => + savedObject._source && savedObject._source.title !== savedObject.title; + + savedObject.creationOpts = (opts: Record = {}) => ({ + id: savedObject.id, + migrationVersion: savedObject.migrationVersion, + ...opts, + }); + + savedObject.save = async (opts: SavedObjectSaveOpts) => { + try { + const result = await saveSavedObject(savedObject, config, opts, services); + return Promise.resolve(result); + } catch (e) { + return Promise.reject(e); + } + }; + + savedObject.destroy = () => {}; + + /** + * Delete this object from Elasticsearch + * @return {promise} + */ + savedObject.delete = () => { + if (!savedObject.id) { + return Promise.reject(new Error('Deleting a saved Object requires type and id')); + } + return savedObjectsClient.delete(esType, savedObject.id); + }; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts new file mode 100644 index 00000000000000..5c1c1d0d9a851f --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/check_for_duplicate_title.ts @@ -0,0 +1,69 @@ +/* + * 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 { SavedObject, SavedObjectKibanaServices } from '../types'; +import { findObjectByTitle } from './find_object_by_title'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; + +/** + * check for an existing SavedObject with the same title in ES + * returns Promise when it's no duplicate, or the modal displaying the warning + * that's there's a duplicate is confirmed, else it returns a rejected Promise + * @param savedObject + * @param isTitleDuplicateConfirmed + * @param onTitleDuplicate + * @param services + */ +export async function checkForDuplicateTitle( + savedObject: SavedObject, + isTitleDuplicateConfirmed: boolean, + onTitleDuplicate: (() => void) | undefined, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, overlays } = services; + // Don't check for duplicates if user has already confirmed save with duplicate title + if (isTitleDuplicateConfirmed) { + return true; + } + + // Don't check if the user isn't updating the title, otherwise that would become very annoying to have + // to confirm the save every time, except when copyOnSave is true, then we do want to check. + if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { + return true; + } + + const duplicate = await findObjectByTitle( + savedObjectsClient, + savedObject.getEsType(), + savedObject.title + ); + + if (!duplicate || duplicate.id === savedObject.id) { + return true; + } + + if (onTitleDuplicate) { + onTitleDuplicate(); + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } + + // TODO: make onTitleDuplicate a required prop and remove UI components from this class + // Need to leave here until all users pass onTitleDuplicate. + return displayDuplicateTitleConfirmModal(savedObject, overlays); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx new file mode 100644 index 00000000000000..1e0a4f4ebe47fb --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/confirm_modal_promise.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { OverlayStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal } from '@elastic/eui'; +import { toMountPoint } from '../../../../../plugins/kibana_react/public'; + +export function confirmModalPromise( + message = '', + title = '', + confirmBtnText = '', + overlays: OverlayStart +): Promise { + return new Promise((resolve, reject) => { + const cancelButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ); + + const modal = overlays.openModal( + toMountPoint( + { + modal.close(); + reject(); + }} + onConfirm={() => { + modal.close(); + resolve(true); + }} + confirmButtonText={confirmBtnText} + cancelButtonText={cancelButtonText} + title={title} + > + {message} + + ) + ); + }); +} diff --git a/src/legacy/ui/public/saved_objects/helpers/create_source.ts b/src/legacy/ui/public/saved_objects/helpers/create_source.ts new file mode 100644 index 00000000000000..1818671cecebe5 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/create_source.ts @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { SavedObjectAttributes } from 'kibana/public'; +import { OVERWRITE_REJECTED } from 'ui/saved_objects/constants'; +import { confirmModalPromise } from './confirm_modal_promise'; + +/** + * Attempts to create the current object using the serialized source. If an object already + * exists, a warning message requests an overwrite confirmation. + * @param source - serialized version of this object (return value from this._serialize()) + * What will be indexed into elasticsearch. + * @param savedObject - savedObject + * @param esType - type of the saved object + * @param options - options to pass to the saved object create method + * @param services - provides Kibana services savedObjectsClient and overlays + * @returns {Promise} - A promise that is resolved with the objects id if the object is + * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with + * a confirmRejected = true parameter so that case can be handled differently than + * a create or index error. + * @resolved {SavedObject} + */ +export async function createSource( + source: SavedObjectAttributes, + savedObject: SavedObject, + esType: string, + options = {}, + services: SavedObjectKibanaServices +) { + const { savedObjectsClient, overlays } = services; + try { + return await savedObjectsClient.create(esType, source, options); + } catch (err) { + // record exists, confirm overwriting + if (_.get(err, 'res.status') === 409) { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', + { + defaultMessage: 'Are you sure you want to overwrite {title}?', + values: { title: savedObject.title }, + } + ); + + const title = i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { + defaultMessage: 'Overwrite {name}?', + values: { name: savedObject.getDisplayName() }, + }); + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.overwriteButtonLabel', + { + defaultMessage: 'Overwrite', + } + ); + + return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays) + .then(() => + savedObjectsClient.create( + esType, + source, + savedObject.creationOpts({ overwrite: true, ...options }) + ) + ) + .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); + } + return await Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts new file mode 100644 index 00000000000000..36882db72d56cc --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/display_duplicate_title_confirm_modal.ts @@ -0,0 +1,49 @@ +/* + * 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 { OverlayStart } from 'kibana/public'; +import { SAVE_DUPLICATE_REJECTED } from '../constants'; +import { confirmModalPromise } from './confirm_modal_promise'; +import { SavedObject } from '../types'; + +export function displayDuplicateTitleConfirmModal( + savedObject: SavedObject, + overlays: OverlayStart +): Promise { + const confirmMessage = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', + { + defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, + values: { title: savedObject.title, name: savedObject.getDisplayName() }, + } + ); + + const confirmButtonText = i18n.translate( + 'common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', + { + defaultMessage: 'Save {name}', + values: { name: savedObject.getDisplayName() }, + } + ); + try { + return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays); + } catch (_) { + return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); + } +} diff --git a/src/legacy/ui/public/saved_objects/find_object_by_title.ts b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts similarity index 75% rename from src/legacy/ui/public/saved_objects/find_object_by_title.ts rename to src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts index d6f11bcb809565..373800f5766278 100644 --- a/src/legacy/ui/public/saved_objects/find_object_by_title.ts +++ b/src/legacy/ui/public/saved_objects/helpers/find_object_by_title.ts @@ -17,7 +17,6 @@ * under the License. */ -import { find } from 'lodash'; import { SavedObjectAttributes } from 'src/core/server'; import { SavedObjectsClientContract } from 'src/core/public'; import { SimpleSavedObject } from 'src/core/public'; @@ -30,30 +29,23 @@ import { SimpleSavedObject } from 'src/core/public'; * @param title {string} * @returns {Promise} */ -export function findObjectByTitle( +export async function findObjectByTitle( savedObjectsClient: SavedObjectsClientContract, type: string, title: string ): Promise | void> { if (!title) { - return Promise.resolve(); + return; } // Elastic search will return the most relevant results first, which means exact matches should come // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - return savedObjectsClient - .find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }) - .then(response => { - const match = find(response.savedObjects, obj => { - return obj.get('title').toLowerCase() === title.toLowerCase(); - }); - - return match; - }); + const response = await savedObjectsClient.find({ + type, + perPage: 10, + search: `"${title}"`, + searchFields: ['title'], + fields: ['title'], + }); + return response.savedObjects.find(obj => obj.get('title').toLowerCase() === title.toLowerCase()); } diff --git a/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts new file mode 100644 index 00000000000000..a78b3f97e884be --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/hydrate_index_pattern.ts @@ -0,0 +1,55 @@ +/* + * 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 { SavedObject, SavedObjectConfig } from '../types'; +import { IndexPatternsContract } from '../../../../../plugins/data/public'; + +/** + * After creation or fetching from ES, ensure that the searchSources index indexPattern + * is an bonafide IndexPattern object. + * + * @return {Promise} + */ +export async function hydrateIndexPattern( + id: string, + savedObject: SavedObject, + indexPatterns: IndexPatternsContract, + config: SavedObjectConfig +) { + const clearSavedIndexPattern = !!config.clearSavedIndexPattern; + const indexPattern = config.indexPattern; + + if (!savedObject.searchSource) { + return null; + } + + if (clearSavedIndexPattern) { + savedObject.searchSource!.setField('index', undefined); + return null; + } + + const index = id || indexPattern || savedObject.searchSource!.getOwnField('index'); + + if (typeof index !== 'string' || !index) { + return null; + } + + const indexObj = await indexPatterns.get(index); + savedObject.searchSource!.setField('index', indexObj); + return indexObj; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts new file mode 100644 index 00000000000000..c5ea31e0784aad --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/initialize_saved_object.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { SavedObjectsClientContract } from 'kibana/public'; +import { SavedObject, SavedObjectConfig } from '../types'; + +/** + * Initialize saved object + */ +export async function intializeSavedObject( + savedObject: SavedObject, + savedObjectsClient: SavedObjectsClientContract, + config: SavedObjectConfig +) { + const esType = config.type; + // ensure that the esType is defined + if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); + + if (!savedObject.id) { + // just assign the defaults and be done + _.assign(savedObject, savedObject.defaults); + await savedObject.hydrateIndexPattern!(); + if (typeof config.afterESResp === 'function') { + await config.afterESResp.call(savedObject); + } + return savedObject; + } + + const resp = await savedObjectsClient.get(esType, savedObject.id); + const respMapped = { + _id: resp.id, + _type: resp.type, + _source: _.cloneDeep(resp.attributes), + references: resp.references, + found: !!resp._version, + }; + await savedObject.applyESResp(respMapped); + if (typeof config.init === 'function') { + await config.init.call(savedObject); + } + + return savedObject; +} diff --git a/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts new file mode 100644 index 00000000000000..8c52b7cfa0dbfe --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/parse_search_source.ts @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import { migrateLegacyQuery } from 'ui/utils/migrate_legacy_query'; +import { SavedObject } from '../types'; +import { InvalidJSONProperty } from '../../../../../plugins/kibana_utils/public'; + +export function parseSearchSource( + savedObject: SavedObject, + esType: string, + searchSourceJson: string, + references: any[] +) { + if (!savedObject.searchSource) return; + + // if we have a searchSource, set its values based on the searchSourceJson field + let searchSourceValues: Record; + try { + searchSourceValues = JSON.parse(searchSourceJson); + } catch (e) { + throw new InvalidJSONProperty( + `Invalid JSON in ${esType} "${savedObject.id}". ${e.message} JSON: ${searchSourceJson}` + ); + } + + // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. + // (This happened in issue #20308) + if (!searchSourceValues || typeof searchSourceValues !== 'object') { + throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${savedObject.id}".`); + } + + // Inject index id if a reference is saved + if (searchSourceValues.indexRefName) { + const reference = references.find( + (ref: Record) => ref.name === searchSourceValues.indexRefName + ); + if (!reference) { + throw new Error( + `Could not find reference for ${ + searchSourceValues.indexRefName + } on ${savedObject.getEsType()} ${savedObject.id}` + ); + } + searchSourceValues.index = reference.id; + delete searchSourceValues.indexRefName; + } + + if (searchSourceValues.filter) { + searchSourceValues.filter.forEach((filterRow: any) => { + if (!filterRow.meta || !filterRow.meta.indexRefName) { + return; + } + const reference = references.find((ref: any) => ref.name === filterRow.meta.indexRefName); + if (!reference) { + throw new Error( + `Could not find reference for ${ + filterRow.meta.indexRefName + } on ${savedObject.getEsType()}` + ); + } + filterRow.meta.index = reference.id; + delete filterRow.meta.indexRefName; + }); + } + + const searchSourceFields = savedObject.searchSource.getFields(); + const fnProps = _.transform( + searchSourceFields, + function(dynamic: Record, val: any, name: string | undefined) { + if (_.isFunction(val) && name) dynamic[name] = val; + }, + {} + ); + + savedObject.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); + const query = savedObject.searchSource.getOwnField('query'); + + if (typeof query !== 'undefined') { + savedObject.searchSource.setField('query', migrateLegacyQuery(query)); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts new file mode 100644 index 00000000000000..bd6daa1832a255 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/save_saved_object.ts @@ -0,0 +1,128 @@ +/* + * 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 { + SavedObject, + SavedObjectConfig, + SavedObjectKibanaServices, + SavedObjectSaveOpts, +} from '../types'; +import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from '../constants'; +import { createSource } from './create_source'; +import { checkForDuplicateTitle } from './check_for_duplicate_title'; + +/** + * @param error {Error} the error + * @return {boolean} + */ +function isErrorNonFatal(error: { message: string }) { + if (!error) return false; + return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; +} + +/** + * Saves this object. + * + * @param {string} [esType] + * @param {SavedObject} [savedObject] + * @param {SavedObjectConfig} [config] + * @param {object} [options={}] + * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it + * can confirm an overwrite if a document with the id already exists. + * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title + * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. + * When not provided, confirm modal will be displayed asking user to confirm or cancel save. + * @param {SavedObjectKibanaServices} [services] + * @return {Promise} + * @resolved {String} - The id of the doc + */ +export async function saveSavedObject( + savedObject: SavedObject, + config: SavedObjectConfig, + { + confirmOverwrite = false, + isTitleDuplicateConfirmed = false, + onTitleDuplicate, + }: SavedObjectSaveOpts = {}, + services: SavedObjectKibanaServices +): Promise { + const { savedObjectsClient, chrome } = services; + + const esType = config.type || ''; + const extractReferences = config.extractReferences; + // Save the original id in case the save fails. + const originalId = savedObject.id; + // Read https://github.com/elastic/kibana/issues/9056 and + // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable + // exists. + // The goal is to move towards a better rename flow, but since our users have been conditioned + // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better + // UI/UX can be worked out. + if (savedObject.copyOnSave) { + delete savedObject.id; + } + + // Here we want to extract references and set them within "references" attribute + let { attributes, references } = savedObject._serialize(); + if (extractReferences) { + ({ attributes, references } = extractReferences({ attributes, references })); + } + if (!references) throw new Error('References not returned from extractReferences'); + + try { + await checkForDuplicateTitle( + savedObject, + isTitleDuplicateConfirmed, + onTitleDuplicate, + services + ); + savedObject.isSaving = true; + const resp = confirmOverwrite + ? await createSource( + attributes, + savedObject, + esType, + savedObject.creationOpts({ references }), + services + ) + : await savedObjectsClient.create( + esType, + attributes, + savedObject.creationOpts({ references, overwrite: true }) + ); + + savedObject.id = resp.id; + if (savedObject.showInRecentlyAccessed && savedObject.getFullPath) { + chrome.recentlyAccessed.add( + savedObject.getFullPath(), + savedObject.title, + String(savedObject.id) + ); + } + savedObject.isSaving = false; + savedObject.lastSavedTitle = savedObject.title; + return savedObject.id; + } catch (err) { + savedObject.isSaving = false; + savedObject.id = originalId; + if (isErrorNonFatal(err)) { + return ''; + } + return Promise.reject(err); + } +} diff --git a/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts new file mode 100644 index 00000000000000..ca780f8f9584d4 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/helpers/serialize_saved_object.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import _ from 'lodash'; +import angular from 'angular'; +import { SavedObject, SavedObjectConfig } from '../types'; +import { expandShorthand } from '../../../../../plugins/kibana_utils/public'; + +export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { + // mapping definition for the fields that this object will expose + const mapping = expandShorthand(config.mapping); + const attributes = {} as Record; + const references = []; + + _.forOwn(mapping, (fieldMapping, fieldName) => { + if (typeof fieldName !== 'string') { + return; + } + // @ts-ignore + const savedObjectFieldVal = savedObject[fieldName]; + if (savedObjectFieldVal != null) { + attributes[fieldName] = fieldMapping._serialize + ? fieldMapping._serialize(savedObjectFieldVal) + : savedObjectFieldVal; + } + }); + + if (savedObject.searchSource) { + let searchSourceFields: Record = _.omit(savedObject.searchSource.getFields(), [ + 'sort', + 'size', + ]); + if (searchSourceFields.index) { + // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: + // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on Saved Object + // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` + const indexId = + typeof searchSourceFields.index === 'string' + ? searchSourceFields.index + : searchSourceFields.index.id; + const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + references.push({ + name: refName, + type: 'index-pattern', + id: indexId, + }); + searchSourceFields = { + ...searchSourceFields, + indexRefName: refName, + index: undefined, + }; + } + if (searchSourceFields.filter) { + searchSourceFields = { + ...searchSourceFields, + filter: searchSourceFields.filter.map((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return filterRow; + } + const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + references.push({ + name: refName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + return { + ...filterRow, + meta: { + ...filterRow.meta, + indexRefName: refName, + index: undefined, + }, + }; + }), + }; + } + attributes.kibanaSavedObjectMeta = { + searchSourceJSON: angular.toJson(searchSourceFields), + }; + } + + return { attributes, references }; +} diff --git a/src/legacy/ui/public/saved_objects/index.ts b/src/legacy/ui/public/saved_objects/index.ts index 8076213f62e9a2..3c77a02c608c65 100644 --- a/src/legacy/ui/public/saved_objects/index.ts +++ b/src/legacy/ui/public/saved_objects/index.ts @@ -19,6 +19,5 @@ export { SavedObjectRegistryProvider } from './saved_object_registry'; export { SavedObjectsClientProvider } from './saved_objects_client_provider'; -// @ts-ignore export { SavedObjectLoader } from './saved_object_loader'; -export { findObjectByTitle } from './find_object_by_title'; +export { findObjectByTitle } from './helpers/find_object_by_title'; diff --git a/src/legacy/ui/public/saved_objects/saved_object.js b/src/legacy/ui/public/saved_objects/saved_object.js deleted file mode 100644 index 1db651ad9308f5..00000000000000 --- a/src/legacy/ui/public/saved_objects/saved_object.js +++ /dev/null @@ -1,541 +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. - */ - -/** - * @name SavedObject - * - * NOTE: SavedObject seems to track a reference to an object in ES, - * and surface methods for CRUD functionality (save and delete). This seems - * similar to how Backbone Models work. - * - * This class seems to interface with ES primarily through the es Angular - * service and the saved object api. - */ - -import angular from 'angular'; -import _ from 'lodash'; - - -import { InvalidJSONProperty, SavedObjectNotFound, expandShorthand } from '../../../../plugins/kibana_utils/public'; - -import { SearchSource } from '../courier'; -import { findObjectByTitle } from './find_object_by_title'; -import { SavedObjectsClientProvider } from './saved_objects_client_provider'; -import { migrateLegacyQuery } from '../utils/migrate_legacy_query'; -import { npStart } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; - -/** - * An error message to be used when the user rejects a confirm overwrite. - * @type {string} - */ -const OVERWRITE_REJECTED = i18n.translate('common.ui.savedObjects.overwriteRejectedDescription', { - defaultMessage: 'Overwrite confirmation was rejected' -}); - -/** - * An error message to be used when the user rejects a confirm save with duplicate title. - * @type {string} - */ -const SAVE_DUPLICATE_REJECTED = i18n.translate('common.ui.savedObjects.saveDuplicateRejectedDescription', { - defaultMessage: 'Save with duplicate title confirmation was rejected' -}); - -/** - * @param error {Error} the error - * @return {boolean} - */ -function isErrorNonFatal(error) { - if (!error) return false; - return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; -} - -export function SavedObjectProvider(Promise, Private, confirmModalPromise, indexPatterns) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - - /** - * The SavedObject class is a base class for saved objects loaded from the server and - * provides additional functionality besides loading/saving/deleting/etc. - * - * It is overloaded and configured to provide type-aware functionality. - * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader - * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. - * @param {*} config - */ - function SavedObject(config) { - if (!_.isObject(config)) config = {}; - - /************ - * Initialize config vars - ************/ - - // type name for this object, used as the ES-type - const esType = config.type; - - this.getDisplayName = function () { - return esType; - }; - - // NOTE: this.type (not set in this file, but somewhere else) is the sub type, e.g. 'area' or - // 'data table', while esType is the more generic type - e.g. 'visualization' or 'saved search'. - this.getEsType = function () { - return esType; - }; - - /** - * Flips to true during a save operation, and back to false once the save operation - * completes. - * @type {boolean} - */ - this.isSaving = false; - this.defaults = config.defaults || {}; - - // mapping definition for the fields that this object will expose - const mapping = expandShorthand(config.mapping); - - const afterESResp = config.afterESResp || _.noop; - const customInit = config.init || _.noop; - const extractReferences = config.extractReferences; - const injectReferences = config.injectReferences; - - // optional search source which this object configures - this.searchSource = config.searchSource ? new SearchSource() : undefined; - - // the id of the document - this.id = config.id || void 0; - - // the migration version of the document, should only be set on imports - this.migrationVersion = config.migrationVersion; - - // Whether to create a copy when the object is saved. This should eventually go away - // in favor of a better rename/save flow. - this.copyOnSave = false; - - const parseSearchSource = (searchSourceJson, references) => { - if (!this.searchSource) return; - - // if we have a searchSource, set its values based on the searchSourceJson field - let searchSourceValues; - try { - searchSourceValues = JSON.parse(searchSourceJson); - } catch (e) { - throw new InvalidJSONProperty( - `Invalid JSON in ${esType} "${this.id}". ${e.message} JSON: ${searchSourceJson}` - ); - } - - // This detects a scenario where documents with invalid JSON properties have been imported into the saved object index. - // (This happened in issue #20308) - if (!searchSourceValues || typeof searchSourceValues !== 'object') { - throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${this.id}".`); - } - - // Inject index id if a reference is saved - if (searchSourceValues.indexRefName) { - const reference = references.find(reference => reference.name === searchSourceValues.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${searchSourceValues.indexRefName} on ${this.getEsType()} ${this.id}`); - } - searchSourceValues.index = reference.id; - delete searchSourceValues.indexRefName; - } - - if (searchSourceValues.filter) { - searchSourceValues.filter.forEach((filterRow) => { - if (!filterRow.meta || !filterRow.meta.indexRefName) { - return; - } - const reference = references.find(reference => reference.name === filterRow.meta.indexRefName); - if (!reference) { - throw new Error(`Could not find reference for ${filterRow.meta.indexRefName} on ${this.getEsType()}`); - } - filterRow.meta.index = reference.id; - delete filterRow.meta.indexRefName; - }); - } - - const searchSourceFields = this.searchSource.getFields(); - const fnProps = _.transform(searchSourceFields, function (dynamic, val, name) { - if (_.isFunction(val)) dynamic[name] = val; - }, {}); - - this.searchSource.setFields(_.defaults(searchSourceValues, fnProps)); - - if (!_.isUndefined(this.searchSource.getOwnField('query'))) { - this.searchSource.setField('query', migrateLegacyQuery(this.searchSource.getOwnField('query'))); - } - }; - - /** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ - this.hydrateIndexPattern = (id) => { - if (!this.searchSource) { - return Promise.resolve(null); - } - - if (config.clearSavedIndexPattern) { - this.searchSource.setField('index', null); - return Promise.resolve(null); - } - - let index = id || config.indexPattern || this.searchSource.getOwnField('index'); - - if (!index) { - return Promise.resolve(null); - } - - // If index is not an IndexPattern object at this point, then it's a string id of an index. - if (typeof index === 'string') { - index = indexPatterns.get(index); - } - - // At this point index will either be an IndexPattern, if cached, or a promise that - // will return an IndexPattern, if not cached. - return Promise.resolve(index).then(indexPattern => { - this.searchSource.setField('index', indexPattern); - }); - }; - - /** - * Asynchronously initialize this object - will only run - * once even if called multiple times. - * - * @return {Promise} - * @resolved {SavedObject} - */ - this.init = _.once(() => { - // ensure that the esType is defined - if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); - - return Promise.resolve() - .then(() => { - // If there is not id, then there is no document to fetch from elasticsearch - if (!this.id) { - // just assign the defaults and be done - _.assign(this, this.defaults); - return this.hydrateIndexPattern().then(() => { - return afterESResp.call(this); - }); - } - - // fetch the object from ES - return savedObjectsClient.get(esType, this.id) - .then(resp => { - // temporary compatability for savedObjectsClient - return { - _id: resp.id, - _type: resp.type, - _source: _.cloneDeep(resp.attributes), - references: resp.references, - found: resp._version ? true : false - }; - }) - .then(this.applyESResp) - .catch(this.applyEsResp); - }) - .then(() => customInit.call(this)) - .then(() => this); - }); - - - this.applyESResp = (resp) => { - this._source = _.cloneDeep(resp._source); - - if (resp.found != null && !resp.found) { - throw new SavedObjectNotFound(esType, this.id); - } - - const meta = resp._source.kibanaSavedObjectMeta || {}; - delete resp._source.kibanaSavedObjectMeta; - - if (!config.indexPattern && this._source.indexPattern) { - config.indexPattern = this._source.indexPattern; - delete this._source.indexPattern; - } - - // assign the defaults to the response - _.defaults(this._source, this.defaults); - - // transform the source using _deserializers - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (fieldMapping._deserialize) { - this._source[fieldName] = fieldMapping._deserialize(this._source[fieldName], resp, fieldName, fieldMapping); - } - }); - - // Give obj all of the values in _source.fields - _.assign(this, this._source); - this.lastSavedTitle = this.title; - - return Promise.try(() => { - parseSearchSource(meta.searchSourceJSON, resp.references); - return this.hydrateIndexPattern(); - }).then(() => { - if (injectReferences && resp.references && resp.references.length > 0) { - injectReferences(this, resp.references); - } - return this; - }).then(() => { - return Promise.cast(afterESResp.call(this, resp)); - }); - }; - - /** - * Serialize this object - * - * @return {Object} - */ - this._serialize = () => { - const attributes = {}; - const references = []; - - _.forOwn(mapping, (fieldMapping, fieldName) => { - if (this[fieldName] != null) { - attributes[fieldName] = (fieldMapping._serialize) - ? fieldMapping._serialize(this[fieldName]) - : this[fieldName]; - } - }); - - if (this.searchSource) { - let searchSourceFields = _.omit(this.searchSource.getFields(), ['sort', 'size']); - if (searchSourceFields.index) { - // searchSourceFields.index will normally be an IndexPattern, but can be a string in two scenarios: - // (1) `init()` (and by extension `hydrateIndexPattern()`) hasn't been called on this Saved Object - // (2) The IndexPattern doesn't exist, so we fail to resolve it in `hydrateIndexPattern()` - const indexId = typeof (searchSourceFields.index) === 'string' ? searchSourceFields.index : searchSourceFields.index.id; - const refName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - references.push({ - name: refName, - type: 'index-pattern', - id: indexId, - }); - searchSourceFields = { - ...searchSourceFields, - indexRefName: refName, - index: undefined, - }; - } - if (searchSourceFields.filter) { - searchSourceFields = { - ...searchSourceFields, - filter: searchSourceFields.filter.map((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return filterRow; - } - const refName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - references.push({ - name: refName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - return { - ...filterRow, - meta: { - ...filterRow.meta, - indexRefName: refName, - index: undefined, - } - }; - }), - }; - } - attributes.kibanaSavedObjectMeta = { - searchSourceJSON: angular.toJson(searchSourceFields) - }; - } - - return { attributes, references }; - }; - - /** - * Returns true if the object's original title has been changed. New objects return false. - * @return {boolean} - */ - this.isTitleChanged = () => { - return this._source && this._source.title !== this.title; - }; - - this.creationOpts = (opts = {}) => ({ - id: this.id, - migrationVersion: this.migrationVersion, - ...opts, - }); - - /** - * Attempts to create the current object using the serialized source. If an object already - * exists, a warning message requests an overwrite confirmation. - * @param source - serialized version of this object (return value from this._serialize()) - * What will be indexed into elasticsearch. - * @param options - options to pass to the saved object create method - * @returns {Promise} - A promise that is resolved with the objects id if the object is - * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with - * a confirmRejected = true parameter so that case can be handled differently than - * a create or index error. - * @resolved {SavedObject} - */ - const createSource = (source, options = {}) => { - return savedObjectsClient.create(esType, source, options) - .catch(err => { - // record exists, confirm overwriting - if (_.get(err, 'res.status') === 409) { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.overwriteConfirmationMessage', { - defaultMessage: 'Are you sure you want to overwrite {title}?', - values: { title: this.title } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.overwriteButtonLabel', { - defaultMessage: 'Overwrite', - }), - title: i18n.translate('common.ui.savedObjects.confirmModal.overwriteTitle', { - defaultMessage: 'Overwrite {name}?', - values: { name: this.getDisplayName() } - }), - }) - .then(() => savedObjectsClient.create(esType, source, this.creationOpts({ overwrite: true, ...options }))) - .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); - } - return Promise.reject(err); - }); - }; - - const displayDuplicateTitleConfirmModal = () => { - const confirmMessage = i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateConfirmationMessage', { - defaultMessage: `A {name} with the title '{title}' already exists. Would you like to save anyway?`, - values: { title: this.title, name: this.getDisplayName() } - }); - - return confirmModalPromise(confirmMessage, { - confirmButtonText: i18n.translate('common.ui.savedObjects.confirmModal.saveDuplicateButtonLabel', { - defaultMessage: 'Save {name}', - values: { name: this.getDisplayName() } - }) - }) - .catch(() => Promise.reject(new Error(SAVE_DUPLICATE_REJECTED))); - }; - - const checkForDuplicateTitle = (isTitleDuplicateConfirmed, onTitleDuplicate) => { - // Don't check for duplicates if user has already confirmed save with duplicate title - if (isTitleDuplicateConfirmed) { - return Promise.resolve(); - } - - // Don't check if the user isn't updating the title, otherwise that would become very annoying to have - // to confirm the save every time, except when copyOnSave is true, then we do want to check. - if (this.title === this.lastSavedTitle && !this.copyOnSave) { - return Promise.resolve(); - } - - return findObjectByTitle(savedObjectsClient, this.getEsType(), this.title) - .then(duplicate => { - if (!duplicate) return true; - if (duplicate.id === this.id) return true; - - if (onTitleDuplicate) { - onTitleDuplicate(); - return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); - } - - // TODO: make onTitleDuplicate a required prop and remove UI components from this class - // Need to leave here until all users pass onTitleDuplicate. - return displayDuplicateTitleConfirmModal(); - }); - }; - - /** - * Saves this object. - * - * @param {object} [options={}] - * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @return {Promise} - * @resolved {String} - The id of the doc - */ - this.save = ({ confirmOverwrite = false, isTitleDuplicateConfirmed = false, onTitleDuplicate } = {}) => { - // Save the original id in case the save fails. - const originalId = this.id; - // Read https://github.com/elastic/kibana/issues/9056 and - // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable - // exists. - // The goal is to move towards a better rename flow, but since our users have been conditioned - // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better - // UI/UX can be worked out. - if (this.copyOnSave) { - this.id = null; - } - - // Here we want to extract references and set them within "references" attribute - let { attributes, references } = this._serialize(); - if (extractReferences) { - ({ attributes, references } = extractReferences({ attributes, references })); - } - if (!references) throw new Error('References not returned from extractReferences'); - - this.isSaving = true; - - return checkForDuplicateTitle(isTitleDuplicateConfirmed, onTitleDuplicate) - .then(() => { - if (confirmOverwrite) { - return createSource(attributes, this.creationOpts({ references })); - } else { - return savedObjectsClient.create(esType, attributes, this.creationOpts({ references, overwrite: true })); - } - }) - .then((resp) => { - this.id = resp.id; - }) - .then(() => { - if (this.showInRecentlyAccessed && this.getFullPath) { - npStart.core.chrome.recentlyAccessed.add(this.getFullPath(), this.title, this.id); - } - this.isSaving = false; - this.lastSavedTitle = this.title; - return this.id; - }) - .catch((err) => { - this.isSaving = false; - this.id = originalId; - if (isErrorNonFatal(err)) { - return; - } - return Promise.reject(err); - }); - }; - - this.destroy = () => {}; - - /** - * Delete this object from Elasticsearch - * @return {promise} - */ - this.delete = () => { - return savedObjectsClient.delete(esType, this.id); - }; - } - - return SavedObject; -} diff --git a/src/legacy/ui/public/saved_objects/saved_object.ts b/src/legacy/ui/public/saved_objects/saved_object.ts new file mode 100644 index 00000000000000..91182e67aac0d1 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/saved_object.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * @name SavedObject + * + * NOTE: SavedObject seems to track a reference to an object in ES, + * and surface methods for CRUD functionality (save and delete). This seems + * similar to how Backbone Models work. + * + * This class seems to interface with ES primarily through the es Angular + * service and the saved object api. + */ +import { npStart } from 'ui/new_platform'; +import { SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from './types'; +import { buildSavedObject } from './helpers/build_saved_object'; + +export function createSavedObjectClass(services: SavedObjectKibanaServices) { + /** + * The SavedObject class is a base class for saved objects loaded from the server and + * provides additional functionality besides loading/saving/deleting/etc. + * + * It is overloaded and configured to provide type-aware functionality. + * To just retrieve the attributes of saved objects, it is recommended to use SavedObjectLoader + * which returns instances of SimpleSavedObject which don't introduce additional type-specific complexity. + * @param {*} config + */ + class SavedObjectClass { + constructor(config: SavedObjectConfig = {}) { + // @ts-ignore + const self: SavedObject = this; + buildSavedObject(self, config, services); + } + } + + return SavedObjectClass as new (config: SavedObjectConfig) => SavedObject; +} +// the old angular way, should be removed once no longer used +export function SavedObjectProvider() { + const services = { + savedObjectsClient: npStart.core.savedObjects.client, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + return createSavedObjectClass(services); +} diff --git a/src/legacy/ui/public/saved_objects/saved_object_loader.js b/src/legacy/ui/public/saved_objects/saved_object_loader.ts similarity index 60% rename from src/legacy/ui/public/saved_objects/saved_object_loader.js rename to src/legacy/ui/public/saved_objects/saved_object_loader.ts index 434ce0d8b0caac..eb880ce5380c0c 100644 --- a/src/legacy/ui/public/saved_objects/saved_object_loader.js +++ b/src/legacy/ui/public/saved_objects/saved_object_loader.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ - +import { SavedObject } from 'ui/saved_objects/types'; +import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public'; import { StringUtils } from '../utils/string_utils'; /** @@ -28,20 +29,25 @@ import { StringUtils } from '../utils/string_utils'; * to avoid pulling in extra functionality which isn't used. */ export class SavedObjectLoader { - constructor(SavedObjectClass, kbnUrl, chrome, savedObjectClient) { + private readonly Class: (id: string) => SavedObject; + public type: string; + public lowercaseType: string; + public loaderProperties: Record; + + constructor( + SavedObjectClass: any, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly chrome: ChromeStart + ) { this.type = SavedObjectClass.type; this.Class = SavedObjectClass; this.lowercaseType = this.type.toLowerCase(); - this.kbnUrl = kbnUrl; - this.chrome = chrome; this.loaderProperties = { - name: `${ this.lowercaseType }s`, + name: `${this.lowercaseType}s`, noun: StringUtils.upperFirst(this.type), - nouns: `${ this.lowercaseType }s`, + nouns: `${this.lowercaseType}s`, }; - - this.savedObjectsClient = savedObjectClient; } /** @@ -50,27 +56,38 @@ export class SavedObjectLoader { * @param id * @returns {Promise} */ - get(id) { - return (new this.Class(id)).init(); + async get(id: string) { + // @ts-ignore + const obj = new this.Class(id); + return obj.init(); } - urlFor(id) { - return this.kbnUrl.eval(`#/${ this.lowercaseType }/{{id}}`, { id: id }); + urlFor(id: string) { + return `#/${this.lowercaseType}/${encodeURIComponent(id)}`; } - delete(ids) { - ids = !Array.isArray(ids) ? [ids] : ids; + async delete(ids: string | string[]) { + const idsUsed = !Array.isArray(ids) ? [ids] : ids; - const deletions = ids.map(id => { + const deletions = idsUsed.map(id => { + // @ts-ignore const savedObject = new this.Class(id); return savedObject.delete(); }); + await Promise.all(deletions); - return Promise.all(deletions).then(() => { - if (this.chrome) { - this.chrome.untrackNavLinksForDeletedSavedObjects(ids); - } - }); + const coreNavLinks = this.chrome.navLinks; + /** + * Modify last url for deleted saved objects to avoid loading pages with "Could not locate..." + */ + coreNavLinks + .getAll() + .filter( + link => + link.linkToLastSubUrl && + idsUsed.find(deletedId => link.url && link.url.includes(deletedId)) !== undefined + ) + .forEach(link => coreNavLinks.update(link.id, { url: link.baseUrl })); } /** @@ -80,7 +97,7 @@ export class SavedObjectLoader { * @param id * @returns {source} The modified source object, with an id and url field. */ - mapHitSource(source, id) { + mapHitSource(source: Record, id: string) { source.id = id; source.url = this.urlFor(id); return source; @@ -92,7 +109,7 @@ export class SavedObjectLoader { * @param hit * @returns {hit.attributes} The modified hit.attributes object, with an id and url field. */ - mapSavedObjectApiHits(hit) { + mapSavedObjectApiHits(hit: { attributes: Record; id: string }) { return this.mapHitSource(hit.attributes, hit.id); } @@ -100,13 +117,14 @@ export class SavedObjectLoader { * TODO: Rather than use a hardcoded limit, implement pagination. See * https://github.com/elastic/kibana/issues/8044 for reference. * - * @param searchString + * @param search * @param size + * @param fields * @returns {Promise} */ - findAll(search = '', size = 100, fields) { - return this.savedObjectsClient.find( - { + findAll(search: string = '', size: number = 100, fields?: string[]) { + return this.savedObjectsClient + .find({ type: this.lowercaseType, search: search ? `${search}*` : undefined, perPage: size, @@ -114,20 +132,20 @@ export class SavedObjectLoader { searchFields: ['title^3', 'description'], defaultSearchOperator: 'AND', fields, - }).then((resp) => { - return { - total: resp.total, - hits: resp.savedObjects - .map((savedObject) => this.mapSavedObjectApiHits(savedObject)) - }; - }); + } as SavedObjectsFindOptions) + .then(resp => { + return { + total: resp.total, + hits: resp.savedObjects.map(savedObject => this.mapSavedObjectApiHits(savedObject)), + }; + }); } - find(search = '', size = 100) { + find(search: string = '', size: number = 100) { return this.findAll(search, size).then(resp => { return { total: resp.total, - hits: resp.hits.filter(savedObject => !savedObject.error) + hits: resp.hits.filter(savedObject => !savedObject.error), }; }); } diff --git a/src/legacy/ui/public/saved_objects/types.ts b/src/legacy/ui/public/saved_objects/types.ts new file mode 100644 index 00000000000000..bccf73917882a8 --- /dev/null +++ b/src/legacy/ui/public/saved_objects/types.ts @@ -0,0 +1,90 @@ +/* + * 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 { ChromeStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public'; +import { SearchSource, SearchSourceContract } from 'ui/courier'; +import { SavedObjectAttributes, SavedObjectReference } from 'kibana/server'; +import { IndexPatternsContract } from '../../../../plugins/data/public'; +import { IndexPattern } from '../../../core_plugins/data/public'; + +export interface SavedObject { + _serialize: () => { attributes: SavedObjectAttributes; references: SavedObjectReference[] }; + _source: Record; + applyESResp: (resp: EsResponse) => Promise; + copyOnSave: boolean; + creationOpts: (opts: SavedObjectCreationOpts) => Record; + defaults: any; + delete?: () => Promise<{}>; + destroy?: () => void; + getDisplayName: () => string; + getEsType: () => string; + getFullPath: () => string; + hydrateIndexPattern?: (id?: string) => Promise; + id?: string; + init?: () => Promise; + isSaving: boolean; + isTitleChanged: () => boolean; + lastSavedTitle: string; + migrationVersion?: Record; + save: (saveOptions: SavedObjectSaveOpts) => Promise; + searchSource?: SearchSourceContract; + showInRecentlyAccessed: boolean; + title: string; +} + +export interface SavedObjectSaveOpts { + confirmOverwrite?: boolean; + isTitleDuplicateConfirmed?: boolean; + onTitleDuplicate?: () => void; +} + +export interface SavedObjectCreationOpts { + references?: SavedObjectReference[]; + overwrite?: boolean; +} + +export interface SavedObjectKibanaServices { + savedObjectsClient: SavedObjectsClientContract; + indexPatterns: IndexPatternsContract; + chrome: ChromeStart; + overlays: OverlayStart; +} + +export interface SavedObjectConfig { + afterESResp?: () => any; + clearSavedIndexPattern?: boolean; + defaults?: any; + extractReferences?: (opts: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }) => { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; + }; + id?: string; + init?: () => void; + indexPattern?: IndexPattern; + injectReferences?: any; + mapping?: any; + migrationVersion?: Record; + path?: string; + searchSource?: SearchSource | boolean; + type?: string; +} + +export type EsResponse = Record; diff --git a/src/legacy/ui/public/state_management/state.js b/src/legacy/ui/public/state_management/state.js index 27186b42499786..e868abb98c852e 100644 --- a/src/legacy/ui/public/state_management/state.js +++ b/src/legacy/ui/public/state_management/state.js @@ -316,6 +316,27 @@ export function StateProvider(Private, $rootScope, $location, stateManagementCon return this._urlParam; }; + /** + * Returns an object with each property name and value corresponding to the entries in this collection + * excluding fields started from '$', '_' and all methods + * + * @return {object} + */ + State.prototype.toObject = function () { + return _.omit(this, (value, key) => { + return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); + }); + }; + + /** Alias for method 'toObject' + * + * @obsolete Please use 'toObject' method instead + * @return {object} + */ + State.prototype.toJSON = function () { + return this.toObject(); + }; + return State; } diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx b/src/legacy/ui/public/time_buckets/index.d.ts similarity index 91% rename from src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx rename to src/legacy/ui/public/time_buckets/index.d.ts index accaac163acfcf..70b9495b81f0eb 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/index.tsx +++ b/src/legacy/ui/public/time_buckets/index.d.ts @@ -17,5 +17,6 @@ * under the License. */ -export * from './search_bar'; -export * from './create_search_bar'; +declare module 'ui/time_buckets' { + export const TimeBuckets: any; +} diff --git a/src/legacy/ui/public/utils/__tests__/base_object.js b/src/legacy/ui/public/utils/__tests__/base_object.js deleted file mode 100644 index dfc5688c7b2f4f..00000000000000 --- a/src/legacy/ui/public/utils/__tests__/base_object.js +++ /dev/null @@ -1,57 +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 '../../private'; - -import { BaseObject } from '../base_object'; - -describe('Base Object', function () { - beforeEach(ngMock.module('kibana')); - - it('should take an inital set of values', function () { - const baseObject = new BaseObject({ message: 'test' }); - expect(baseObject).to.have.property('message', 'test'); - }); - - it('should serialize attributes to RISON', function () { - const baseObject = new BaseObject(); - baseObject.message = 'Testing... 1234'; - const rison = baseObject.toRISON(); - expect(rison).to.equal('(message:\'Testing... 1234\')'); - }); - - it('should not serialize $$attributes to RISON', function () { - const baseObject = new BaseObject(); - baseObject.$$attributes = { foo: 'bar' }; - baseObject.message = 'Testing... 1234'; - const rison = baseObject.toRISON(); - expect(rison).to.equal('(message:\'Testing... 1234\')'); - }); - - it('should serialize attributes for JSON', function () { - const baseObject = new BaseObject(); - baseObject.message = 'Testing... 1234'; - baseObject._private = 'foo'; - baseObject.$private = 'stuff'; - const json = JSON.stringify(baseObject); - expect(json).to.equal('{"message":"Testing... 1234"}'); - }); -}); diff --git a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts b/src/legacy/ui/public/utils/__tests__/cidr_mask.ts deleted file mode 100644 index 5277344448bd85..00000000000000 --- a/src/legacy/ui/public/utils/__tests__/cidr_mask.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; -import { CidrMask } from '../cidr_mask'; - -describe('CidrMask', () => { - it('should throw errors with invalid CIDR masks', () => { - expect( - () => - // @ts-ignore - new CidrMask() - ).to.throwError(); - - expect(() => new CidrMask('')).to.throwError(); - - expect(() => new CidrMask('hello, world')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/0')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/33')).to.throwError(); - - expect(() => new CidrMask('256.0.0.0/32')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/32/32')).to.throwError(); - - expect(() => new CidrMask('1.2.3/1')).to.throwError(); - - expect(() => new CidrMask('0.0.0.0/123d')).to.throwError(); - }); - - it('should correctly grab IP address and prefix length', () => { - let mask = new CidrMask('0.0.0.0/1'); - expect(mask.initialAddress.toString()).to.be('0.0.0.0'); - expect(mask.prefixLength).to.be(1); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.initialAddress.toString()).to.be('128.0.0.1'); - expect(mask.prefixLength).to.be(31); - }); - - it('should calculate a range of IP addresses', () => { - let mask = new CidrMask('0.0.0.0/1'); - let range = mask.getRange(); - expect(range.from.toString()).to.be('0.0.0.0'); - expect(range.to.toString()).to.be('127.255.255.255'); - - mask = new CidrMask('1.2.3.4/2'); - range = mask.getRange(); - expect(range.from.toString()).to.be('0.0.0.0'); - expect(range.to.toString()).to.be('63.255.255.255'); - - mask = new CidrMask('67.129.65.201/27'); - range = mask.getRange(); - expect(range.from.toString()).to.be('67.129.65.192'); - expect(range.to.toString()).to.be('67.129.65.223'); - }); - - it('toString()', () => { - let mask = new CidrMask('.../1'); - expect(mask.toString()).to.be('0.0.0.0/1'); - - mask = new CidrMask('128.0.0.1/31'); - expect(mask.toString()).to.be('128.0.0.1/31'); - }); -}); diff --git a/src/legacy/ui/public/utils/__tests__/simple_emitter.js b/src/legacy/ui/public/utils/__tests__/simple_emitter.js deleted file mode 100644 index 25224a409f8f4b..00000000000000 --- a/src/legacy/ui/public/utils/__tests__/simple_emitter.js +++ /dev/null @@ -1,175 +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 { SimpleEmitter } from '../simple_emitter'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -describe('SimpleEmitter class', function () { - let emitter; - - beforeEach(function () { - emitter = new SimpleEmitter(); - }); - - it('constructs an event emitter', function () { - expect(emitter).to.have.property('on'); - expect(emitter).to.have.property('off'); - expect(emitter).to.have.property('emit'); - expect(emitter).to.have.property('listenerCount'); - expect(emitter).to.have.property('removeAllListeners'); - }); - - describe('#listenerCount', function () { - it('counts all event listeners without any arg', function () { - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', function () {}); - expect(emitter.listenerCount()).to.be(1); - emitter.on('b', function () {}); - expect(emitter.listenerCount()).to.be(2); - }); - - it('limits to the event that is passed in', function () { - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(1); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - emitter.on('b', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - expect(emitter.listenerCount('b')).to.be(1); - expect(emitter.listenerCount()).to.be(3); - }); - }); - - describe('#on', function () { - it('registers a handler', function () { - const handler = sinon.stub(); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).to.be(1); - - expect(handler.callCount).to.be(0); - emitter.emit('a'); - expect(handler.callCount).to.be(1); - }); - - it('allows multiple event handlers for the same event', function () { - emitter.on('a', function () {}); - emitter.on('a', function () {}); - expect(emitter.listenerCount('a')).to.be(2); - }); - - it('allows the same function to be registered multiple times', function () { - const handler = function () {}; - emitter.on('a', handler); - expect(emitter.listenerCount()).to.be(1); - emitter.on('a', handler); - expect(emitter.listenerCount()).to.be(2); - }); - }); - - describe('#off', function () { - it('removes a listener if it was registered', function () { - const handler = sinon.stub(); - expect(emitter.listenerCount()).to.be(0); - emitter.on('a', handler); - expect(emitter.listenerCount('a')).to.be(1); - emitter.off('a', handler); - expect(emitter.listenerCount('a')).to.be(0); - }); - - it('clears all listeners if no handler is passed', function () { - emitter.on('a', function () {}); - emitter.on('a', function () {}); - expect(emitter.listenerCount()).to.be(2); - emitter.off('a'); - expect(emitter.listenerCount()).to.be(0); - }); - - it('does not mind if the listener is not registered', function () { - emitter.off('a', function () {}); - }); - - it('does not mind if the event has no listeners', function () { - emitter.off('a'); - }); - }); - - describe('#emit', function () { - it('calls the handlers in the order they were defined', function () { - let i = 0; - const incr = function () { return ++i; }; - const one = sinon.spy(incr); - const two = sinon.spy(incr); - const three = sinon.spy(incr); - const four = sinon.spy(incr); - - emitter - .on('a', one) - .on('a', two) - .on('a', three) - .on('a', four) - .emit('a'); - - expect(one).to.have.property('callCount', 1); - expect(one.returned(1)).to.be.ok(); - - expect(two).to.have.property('callCount', 1); - expect(two.returned(2)).to.be.ok(); - - expect(three).to.have.property('callCount', 1); - expect(three.returned(3)).to.be.ok(); - - expect(four).to.have.property('callCount', 1); - expect(four.returned(4)).to.be.ok(); - }); - - it('always emits the handlers that were initially registered', function () { - - const destructive = sinon.spy(function () { - emitter.removeAllListeners(); - expect(emitter.listenerCount()).to.be(0); - }); - const stub = sinon.stub(); - - emitter.on('run', destructive).on('run', stub).emit('run'); - - expect(destructive).to.have.property('callCount', 1); - expect(stub).to.have.property('callCount', 1); - }); - - it('applies all arguments except the first', function () { - emitter - .on('a', function (a, b, c) { - expect(a).to.be('foo'); - expect(b).to.be('bar'); - expect(c).to.be('baz'); - }) - .emit('a', 'foo', 'bar', 'baz'); - }); - - it('uses the SimpleEmitter as the this context', function () { - emitter - .on('a', function () { - expect(this).to.be(emitter); - }) - .emit('a'); - }); - }); -}); diff --git a/src/legacy/ui/public/utils/base_object.ts b/src/legacy/ui/public/utils/base_object.ts deleted file mode 100644 index 63c7ebf6de5bb6..00000000000000 --- a/src/legacy/ui/public/utils/base_object.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import angular from 'angular'; -import _ from 'lodash'; -// @ts-ignore -- awaiting https://github.com/w33ble/rison-node/issues/1 -import rison from 'rison-node'; - -export class BaseObject { - // Set the attributes or default to an empty object - constructor(attributes: Record = {}) { - // Set the attributes or default to an empty object - _.assign(this, attributes); - } - - public toObject() { - // return just the data. - return _.omit(this, (value: any, key: string) => { - return key.charAt(0) === '$' || key.charAt(0) === '_' || _.isFunction(value); - }); - } - - public toRISON() { - // Use Angular to remove the private vars, and JSON.stringify to serialize - return rison.encode(JSON.parse(angular.toJson(this))); - } - - public toJSON() { - return this.toObject(); - } -} diff --git a/src/legacy/ui/public/utils/simple_emitter.js b/src/legacy/ui/public/utils/simple_emitter.js index 84397962c286b0..503798ba160dbb 100644 --- a/src/legacy/ui/public/utils/simple_emitter.js +++ b/src/legacy/ui/public/utils/simple_emitter.js @@ -18,8 +18,6 @@ */ import _ from 'lodash'; -import { BaseObject } from './base_object'; -import { createLegacyClass } from './legacy_class'; /** * Simple event emitter class used in the vislib. Calls @@ -27,7 +25,6 @@ import { createLegacyClass } from './legacy_class'; * * @class */ -createLegacyClass(SimpleEmitter).inherits(BaseObject); export function SimpleEmitter() { this._listeners = {}; } @@ -134,4 +131,3 @@ SimpleEmitter.prototype.listenerCount = function (name) { return count + _.size(handlers); }, 0); }; - diff --git a/src/legacy/ui/public/utils/simple_emitter.test.js b/src/legacy/ui/public/utils/simple_emitter.test.js new file mode 100644 index 00000000000000..ff884a12be7ee8 --- /dev/null +++ b/src/legacy/ui/public/utils/simple_emitter.test.js @@ -0,0 +1,173 @@ +/* + * 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 { SimpleEmitter } from './simple_emitter'; +import sinon from 'sinon'; + +describe('SimpleEmitter class', () => { + let emitter; + + beforeEach(() => { + emitter = new SimpleEmitter(); + }); + + it('constructs an event emitter', () => { + expect(emitter).toHaveProperty('on'); + expect(emitter).toHaveProperty('off'); + expect(emitter).toHaveProperty('emit'); + expect(emitter).toHaveProperty('listenerCount'); + expect(emitter).toHaveProperty('removeAllListeners'); + }); + + describe('#listenerCount', () => { + it('counts all event listeners without any arg', () => { + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', () => {}); + expect(emitter.listenerCount()).toBe(1); + emitter.on('b', () => {}); + expect(emitter.listenerCount()).toBe(2); + }); + + it('limits to the event that is passed in', () => { + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(1); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + emitter.on('b', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + expect(emitter.listenerCount('b')).toBe(1); + expect(emitter.listenerCount()).toBe(3); + }); + }); + + describe('#on', () => { + it('registers a handler', () => { + const handler = sinon.stub(); + emitter.on('a', handler); + expect(emitter.listenerCount('a')).toBe(1); + + expect(handler.callCount).toBe(0); + emitter.emit('a'); + expect(handler.callCount).toBe(1); + }); + + it('allows multiple event handlers for the same event', () => { + emitter.on('a', () => {}); + emitter.on('a', () => {}); + expect(emitter.listenerCount('a')).toBe(2); + }); + + it('allows the same function to be registered multiple times', () => { + const handler = () => {}; + emitter.on('a', handler); + expect(emitter.listenerCount()).toBe(1); + emitter.on('a', handler); + expect(emitter.listenerCount()).toBe(2); + }); + }); + + describe('#off', () => { + it('removes a listener if it was registered', () => { + const handler = sinon.stub(); + expect(emitter.listenerCount()).toBe(0); + emitter.on('a', handler); + expect(emitter.listenerCount('a')).toBe(1); + emitter.off('a', handler); + expect(emitter.listenerCount('a')).toBe(0); + }); + + it('clears all listeners if no handler is passed', () => { + emitter.on('a', () => {}); + emitter.on('a', () => {}); + expect(emitter.listenerCount()).toBe(2); + emitter.off('a'); + expect(emitter.listenerCount()).toBe(0); + }); + + it('does not mind if the listener is not registered', () => { + emitter.off('a', () => {}); + }); + + it('does not mind if the event has no listeners', () => { + emitter.off('a'); + }); + }); + + describe('#emit', () => { + it('calls the handlers in the order they were defined', () => { + let i = 0; + const incr = () => ++i; + const one = sinon.spy(incr); + const two = sinon.spy(incr); + const three = sinon.spy(incr); + const four = sinon.spy(incr); + + emitter + .on('a', one) + .on('a', two) + .on('a', three) + .on('a', four) + .emit('a'); + + expect(one).toHaveProperty('callCount', 1); + expect(one.returned(1)).toBeDefined(); + + expect(two).toHaveProperty('callCount', 1); + expect(two.returned(2)).toBeDefined(); + + expect(three).toHaveProperty('callCount', 1); + expect(three.returned(3)).toBeDefined(); + + expect(four).toHaveProperty('callCount', 1); + expect(four.returned(4)).toBeDefined(); + }); + + it('always emits the handlers that were initially registered', () => { + const destructive = sinon.spy(() => { + emitter.removeAllListeners(); + expect(emitter.listenerCount()).toBe(0); + }); + const stub = sinon.stub(); + + emitter.on('run', destructive).on('run', stub).emit('run'); + + expect(destructive).toHaveProperty('callCount', 1); + expect(stub).toHaveProperty('callCount', 1); + }); + + it('applies all arguments except the first', () => { + emitter + .on('a', (a, b, c) => { + expect(a).toBe('foo'); + expect(b).toBe('bar'); + expect(c).toBe('baz'); + }) + .emit('a', 'foo', 'bar', 'baz'); + }); + + it('uses the SimpleEmitter as the this context', () => { + emitter + .on('a', function () { + expect(this).toBe(emitter); + }) + .emit('a'); + }); + }); +}); diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx index 7d964204ff90ce..b48f07512332ed 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/mask_list.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { EuiFieldText, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CidrMask } from '../../../../../utils/cidr_mask'; +import { CidrMask } from '../../../../../agg_types/buckets/lib/cidr_mask'; import { InputList, InputListConfig, InputObject, InputModel, InputItem } from './input_list'; const EMPTY_STRING = ''; diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap index ab192e6fd3cbb8..4004f8627a8987 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/__snapshots__/number_list.test.tsx.snap @@ -18,7 +18,7 @@ exports[`NumberList should be rendered with default set of props 1`] = ` onChange={[Function]} onDelete={[Function]} range={ - Range { + NumberListRange { "max": 10, "maxInclusive": true, "min": 1, @@ -45,7 +45,7 @@ exports[`NumberList should be rendered with default set of props 1`] = ` onChange={[Function]} onDelete={[Function]} range={ - Range { + NumberListRange { "max": 10, "maxInclusive": true, "min": 1, diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx index 23e671180e9802..777b0a94f0f3d7 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/number_row.tsx @@ -21,7 +21,7 @@ import React, { useCallback } from 'react'; import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Range } from '../../../../../../utils/range'; +import { NumberListRange } from './range'; export interface NumberRowProps { autoFocus: boolean; @@ -29,7 +29,7 @@ export interface NumberRowProps { isInvalid: boolean; labelledbyId: string; model: NumberRowModel; - range: Range; + range: NumberListRange; onBlur(): void; onChange({ id, value }: { id: string; value: string }): void; onDelete(index: string): void; diff --git a/src/legacy/ui/public/utils/__tests__/range.js b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts similarity index 66% rename from src/legacy/ui/public/utils/__tests__/range.js rename to src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts index e7947894d3e221..e9090e5b38ef78 100644 --- a/src/legacy/ui/public/utils/__tests__/range.js +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.test.ts @@ -17,32 +17,30 @@ * under the License. */ -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { parseRange } from '../range'; +import { forOwn } from 'lodash'; +import { parseRange } from './range'; -describe('Range parsing utility', function () { - - it('throws an error for inputs that are not formatted properly', function () { - expect(function () { +describe('Range parsing utility', () => { + test('throws an error for inputs that are not formatted properly', () => { + expect(() => { parseRange(''); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('p10202'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('{0,100}'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange('[0,100'); - }).to.throwException(TypeError); + }).toThrowError(TypeError); - expect(function () { + expect(function() { parseRange(')0,100('); - }).to.throwException(TypeError); + }).toThrowError(TypeError); }); const tests = { @@ -51,52 +49,52 @@ describe('Range parsing utility', function () { min: 0, max: 100, minInclusive: true, - maxInclusive: true + maxInclusive: true, }, within: [ [0, true], [0.0000001, true], [1, true], [99.99999, true], - [100, true] - ] + [100, true], + ], }, '(26.3 , 42]': { props: { min: 26.3, max: 42, minInclusive: false, - maxInclusive: true + maxInclusive: true, }, within: [ [26.2999999, false], [26.3000001, true], [30, true], [41, true], - [42, true] - ] + [42, true], + ], }, '(-50,50)': { props: { min: -50, max: 50, minInclusive: false, - maxInclusive: false + maxInclusive: false, }, within: [ [-50, false], [-49.99999, true], [0, true], [49.99999, true], - [50, false] - ] + [50, false], + ], }, '(Infinity, -Infinity)': { props: { min: -Infinity, max: Infinity, minInclusive: false, - maxInclusive: false + maxInclusive: false, }, within: [ [0, true], @@ -105,25 +103,24 @@ describe('Range parsing utility', function () { [-10000000000, true], [-Infinity, false], [Infinity, false], - ] - } + ], + }, }; - _.forOwn(tests, function (spec, str) { - - describe(str, function () { + forOwn(tests, (spec, str: any) => { + // eslint-disable-next-line jest/valid-describe + describe(str, () => { const range = parseRange(str); - it('creation', function () { - expect(range).to.eql(spec.props); + it('creation', () => { + expect(range).toEqual(spec.props); }); - spec.within.forEach(function (tup) { - it('#within(' + tup[0] + ')', function () { - expect(range.within(tup[0])).to.be(tup[1]); + spec.within.forEach((tup: any[]) => { + it('#within(' + tup[0] + ')', () => { + expect(range.within(tup[0])).toBe(tup[1]); }); }); }); - }); }); diff --git a/src/legacy/ui/public/utils/range.js b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts similarity index 59% rename from src/legacy/ui/public/utils/range.js rename to src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts index 54bd1b19033469..da3b7a61aea9d2 100644 --- a/src/legacy/ui/public/utils/range.js +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/range.ts @@ -17,8 +17,6 @@ * under the License. */ -import _ from 'lodash'; - /** * Regexp portion that matches our number * @@ -44,41 +42,44 @@ const _RE_NUMBER = '(\\-?(?:\\d+(?:\\.\\d+)?|Infinity))'; * * @type {RegExp} */ -const RANGE_RE = new RegExp('^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$'); +const RANGE_RE = new RegExp( + '^\\s*([\\[|\\(])\\s*' + _RE_NUMBER + '\\s*,\\s*' + _RE_NUMBER + '\\s*([\\]|\\)])\\s*$' +); + +export class NumberListRange { + constructor( + public minInclusive: boolean, + public min: number, + public max: number, + public maxInclusive: boolean + ) {} -export function parseRange(input) { + within(n: number): boolean { + if ((this.min === n && !this.minInclusive) || this.min > n) return false; + if ((this.max === n && !this.maxInclusive) || this.max < n) return false; + + return true; + } +} +export function parseRange(input: string): NumberListRange { const match = String(input).match(RANGE_RE); if (!match) { throw new TypeError('expected input to be in interval notation e.g., (100, 200]'); } - return new Range( - match[1] === '[', - parseFloat(match[2]), - parseFloat(match[3]), - match[4] === ']' - ); -} - -function Range(/* minIncl, min, max, maxIncl */) { - const args = _.toArray(arguments); - if (args[1] > args[2]) args.reverse(); + const args = [match[1] === '[', parseFloat(match[2]), parseFloat(match[3]), match[4] === ']']; - this.minInclusive = args[0]; - this.min = args[1]; - this.max = args[2]; - this.maxInclusive = args[3]; -} - -Range.prototype.within = function (n) { - if (this.min === n && !this.minInclusive) return false; - if (this.min > n) return false; - - if (this.max === n && !this.maxInclusive) return false; - if (this.max < n) return false; - - return true; -}; + if (args[1] > args[2]) { + args.reverse(); + } + const [minInclusive, min, max, maxInclusive] = args; + return new NumberListRange( + minInclusive as boolean, + min as number, + max as number, + maxInclusive as boolean + ); +} diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts index c6772cc1087627..89fb5738db379f 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.test.ts @@ -27,12 +27,12 @@ import { getNextModel, getRange, } from './utils'; -import { Range } from '../../../../../../utils/range'; +import { NumberListRange } from './range'; import { NumberRowModel } from './number_row'; describe('NumberList utils', () => { let modelList: NumberRowModel[]; - let range: Range; + let range: NumberListRange; beforeEach(() => { modelList = [ diff --git a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts index 563e8f0a6a9b7f..399253f27445c2 100644 --- a/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts +++ b/src/legacy/ui/public/vis/editors/default/controls/components/number_list/utils.ts @@ -21,7 +21,7 @@ import { last } from 'lodash'; import { i18n } from '@kbn/i18n'; import { htmlIdGenerator } from '@elastic/eui'; -import { parseRange, Range } from '../../../../../../utils/range'; +import { parseRange, NumberListRange } from './range'; import { NumberRowModel } from './number_row'; const EMPTY_STRING = ''; @@ -34,7 +34,7 @@ function parse(value: string) { return isNaN(parsedValue) ? EMPTY_STRING : parsedValue; } -function getRange(range?: string): Range { +function getRange(range?: string): NumberListRange { try { return range ? parseRange(range) : defaultRange; } catch (e) { @@ -42,7 +42,7 @@ function getRange(range?: string): Range { } } -function validateValue(value: number | '', numberRange: Range) { +function validateValue(value: number | '', numberRange: NumberListRange) { const result: { isInvalid: boolean; error?: string } = { isInvalid: false, }; @@ -76,7 +76,7 @@ function validateOrder(list: Array) { return result; } -function getNextModel(list: NumberRowModel[], range: Range): NumberRowModel { +function getNextModel(list: NumberRowModel[], range: NumberListRange): NumberRowModel { const lastValue = last(list).value; let next = Number(lastValue) ? Number(lastValue) + 1 : 1; @@ -104,7 +104,7 @@ function getInitModelList(list: Array): NumberRowModel[] { function getUpdatedModels( numberList: Array, modelList: NumberRowModel[], - numberRange: Range, + numberRange: NumberListRange, invalidOrderModelIndex?: number ): NumberRowModel[] { if (!numberList.length) { diff --git a/src/legacy/ui/public/vis/map/convert_to_geojson.js b/src/legacy/ui/public/vis/map/convert_to_geojson.js index 77896490678ff1..14c282b58beda5 100644 --- a/src/legacy/ui/public/vis/map/convert_to_geojson.js +++ b/src/legacy/ui/public/vis/map/convert_to_geojson.js @@ -17,10 +17,9 @@ * under the License. */ -import { decodeGeoHash } from 'ui/utils/decode_geo_hash'; +import { decodeGeoHash } from './decode_geo_hash'; import { gridDimensions } from './grid_dimensions'; - export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metric }) { let features; diff --git a/src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js b/src/legacy/ui/public/vis/map/decode_geo_hash.test.ts similarity index 75% rename from src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js rename to src/legacy/ui/public/vis/map/decode_geo_hash.test.ts index 1ffe9ca7b4df26..c1ca7e4c803834 100644 --- a/src/legacy/ui/public/utils/__tests__/decode_geo_hash.test.js +++ b/src/legacy/ui/public/vis/map/decode_geo_hash.test.ts @@ -17,27 +17,18 @@ * under the License. */ -import { geohashColumns, decodeGeoHash } from '../decode_geo_hash'; +import { geohashColumns, decodeGeoHash } from './decode_geo_hash'; -test('geohashColumns', function () { +test('geohashColumns', () => { expect(geohashColumns(1)).toBe(8); expect(geohashColumns(2)).toBe(8 * 4); expect(geohashColumns(3)).toBe(8 * 4 * 8); expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4); }); -test('decodeGeoHash', function () { +test('decodeGeoHash', () => { expect(decodeGeoHash('drm3btev3e86')).toEqual({ - latitude: [ - 41.119999922811985, - 41.12000009045005, - 41.12000000663102, - ], - longitude: [ - -71.34000029414892, - -71.3399999588728, - -71.34000012651086, - ], + latitude: [41.119999922811985, 41.12000009045005, 41.12000000663102], + longitude: [-71.34000029414892, -71.3399999588728, -71.34000012651086], }); }); - diff --git a/src/legacy/ui/public/utils/decode_geo_hash.ts b/src/legacy/ui/public/vis/map/decode_geo_hash.ts similarity index 100% rename from src/legacy/ui/public/utils/decode_geo_hash.ts rename to src/legacy/ui/public/vis/map/decode_geo_hash.ts diff --git a/src/legacy/ui/public/vis/map/kibana_map.js b/src/legacy/ui/public/vis/map/kibana_map.js index dc57809b6570fa..cb618444af7cee 100644 --- a/src/legacy/ui/public/vis/map/kibana_map.js +++ b/src/legacy/ui/public/vis/map/kibana_map.js @@ -22,7 +22,7 @@ import { createZoomWarningMsg } from './map_messages'; import L from 'leaflet'; import $ from 'jquery'; import _ from 'lodash'; -import { zoomToPrecision } from '../../utils/zoom_to_precision'; +import { zoomToPrecision } from './zoom_to_precision'; import { i18n } from '@kbn/i18n'; import { ORIGIN } from '../../../../core_plugins/tile_map/common/origin'; diff --git a/src/legacy/ui/public/utils/zoom_to_precision.js b/src/legacy/ui/public/vis/map/zoom_to_precision.ts similarity index 52% rename from src/legacy/ui/public/utils/zoom_to_precision.js rename to src/legacy/ui/public/vis/map/zoom_to_precision.ts index f5c16b640d127e..552c509590286e 100644 --- a/src/legacy/ui/public/utils/zoom_to_precision.js +++ b/src/legacy/ui/public/vis/map/zoom_to_precision.ts @@ -19,39 +19,42 @@ import { geohashColumns } from './decode_geo_hash'; -const maxPrecision = 12; -/** - * Map Leaflet zoom levels to geohash precision levels. - * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. - */ - - +const defaultMaxPrecision = 12; +const minGeoHashPixels = 16; - -const zoomPrecisionMap = {}; -const minGeohashPixels = 16; - -function calculateZoomToPrecisionMap(maxZoom) { +const calculateZoomToPrecisionMap = (maxZoom: number): Map => { + /** + * Map Leaflet zoom levels to geohash precision levels. + * The size of a geohash column-width on the map should be at least `minGeohashPixels` pixels wide. + */ + const zoomPrecisionMap = new Map(); for (let zoom = 0; zoom <= maxZoom; zoom += 1) { - if (typeof zoomPrecisionMap[zoom] === 'number') { + if (typeof zoomPrecisionMap.get(zoom) === 'number') { continue; } + const worldPixels = 256 * Math.pow(2, zoom); - zoomPrecisionMap[zoom] = 1; - for (let precision = 2; precision <= maxPrecision; precision += 1) { + + zoomPrecisionMap.set(zoom, 1); + + for (let precision = 2; precision <= defaultMaxPrecision; precision += 1) { const columns = geohashColumns(precision); - if ((worldPixels / columns) >= minGeohashPixels) { - zoomPrecisionMap[zoom] = precision; + + if (worldPixels / columns >= minGeoHashPixels) { + zoomPrecisionMap.set(zoom, precision); } else { break; } } } -} + return zoomPrecisionMap; +}; + +export function zoomToPrecision(mapZoom: number, maxPrecision: number, maxZoom: number) { + const zoomPrecisionMap = calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21); + const precision = zoomPrecisionMap.get(mapZoom); -export function zoomToPrecision(mapZoom, maxPrecision, maxZoom) { - calculateZoomToPrecisionMap(typeof maxZoom === 'number' ? maxZoom : 21); - return Math.min(zoomPrecisionMap[mapZoom], maxPrecision); + return precision ? Math.min(precision, maxPrecision) : maxPrecision; } diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts index 377e2cd97b72e8..d754c1d3955955 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities.ts @@ -26,10 +26,8 @@ import { SerializedFieldFormat } from 'src/plugins/expressions/public'; import { IFieldFormatId, FieldFormat } from '../../../../../../plugins/data/public'; import { tabifyGetColumns } from '../../../agg_response/tabify/_get_columns'; -import { dateRange } from '../../../utils/date_range'; -import { ipRange } from '../../../utils/ip_range'; -import { DateRangeKey } from '../../../agg_types/buckets/date_range'; -import { IpRangeKey } from '../../../agg_types/buckets/ip_range'; +import { DateRangeKey, convertDateRangeToString } from '../../../agg_types/buckets/date_range'; +import { IpRangeKey, convertIPRangeToString } from '../../../agg_types/buckets/ip_range'; interface TermsFieldFormatParams { otherBucketLabel: string; @@ -120,14 +118,14 @@ export const getFormat: FormatFactory = mapping => { const nestedFormatter = mapping.params as SerializedFieldFormat; const DateRangeFormat = FieldFormat.from((range: DateRangeKey) => { const format = getFieldFormat(nestedFormatter.id, nestedFormatter.params); - return dateRange.toString(range, format.convert.bind(format)); + return convertDateRangeToString(range, format.convert.bind(format)); }); return new DateRangeFormat(); } else if (id === 'ip_range') { const nestedFormatter = mapping.params as SerializedFieldFormat; const IpRangeFormat = FieldFormat.from((range: IpRangeKey) => { const format = getFieldFormat(nestedFormatter.id, nestedFormatter.params); - return ipRange.toString(range, format.convert.bind(format)); + return convertIPRangeToString(range, format.convert.bind(format)); }); return new IpRangeFormat(); } else if (isTermsFieldFormat(mapping) && mapping.params) { diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss b/src/plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss index 0bd356522c7fa1..9efd36b05095e1 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/grid/_dashboard_grid.scss @@ -34,7 +34,7 @@ .dshLayout-isMaximizedPanel { height: 100% !important; /* 1. */ width: 100%; - position: absolute !important; + position: absolute !important; /* 1 */ } /** diff --git a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss index 9575908146d1d8..b446f1e57a895d 100644 --- a/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss +++ b/src/plugins/dashboard_embeddable_container/public/embeddable/viewport/_dashboard_viewport.scss @@ -1,7 +1,6 @@ .dshDashboardViewport { height: 100%; width: 100%; - background-color: $euiColorEmptyShade; } .dshDashboardViewport-withMargins { diff --git a/src/plugins/data/common/es_query/es_query/index.ts b/src/plugins/data/common/es_query/es_query/index.ts index 82cbc543e19dba..39b10be4c75b31 100644 --- a/src/plugins/data/common/es_query/es_query/index.ts +++ b/src/plugins/data/common/es_query/es_query/index.ts @@ -20,7 +20,5 @@ export { buildEsQuery, EsQueryConfig } from './build_es_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; -export { migrateFilter } from './migrate_filter'; export { decorateQuery } from './decorate_query'; -export { filterMatchesIndex } from './filter_matches_index'; export { getEsQueryConfig } from './get_es_query_config'; diff --git a/src/plugins/data/common/es_query/utils/get_display_value.ts b/src/plugins/data/common/es_query/filters/get_display_value.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_display_value.ts rename to src/plugins/data/common/es_query/filters/get_display_value.ts diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.test.ts rename to src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.test.ts diff --git a/src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts b/src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_index_pattern_from_filter.ts rename to src/plugins/data/common/es_query/filters/get_index_pattern_from_filter.ts diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index 403ff2b79b55f9..990d5883594423 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -21,13 +21,14 @@ import { omit, get } from 'lodash'; import { Filter } from './meta_filter'; export * from './build_filters'; -export * from './get_filter_params'; -export * from './get_filter_field'; - export * from './custom_filter'; export * from './exists_filter'; export * from './geo_bounding_box_filter'; export * from './geo_polygon_filter'; +export * from './get_display_value'; +export * from './get_filter_field'; +export * from './get_filter_params'; +export * from './get_index_pattern_from_filter'; export * from './match_all_filter'; export * from './meta_filter'; export * from './missing_filter'; diff --git a/src/plugins/data/common/es_query/index.ts b/src/plugins/data/common/es_query/index.ts index 937fe09903b6bc..e585fda8aff800 100644 --- a/src/plugins/data/common/es_query/index.ts +++ b/src/plugins/data/common/es_query/index.ts @@ -19,6 +19,5 @@ import * as esQuery from './es_query'; import * as esFilters from './filters'; import * as esKuery from './kuery'; -import * as utils from './utils'; -export { esFilters, esQuery, utils, esKuery }; +export { esFilters, esQuery, esKuery }; diff --git a/src/plugins/data/common/es_query/kuery/functions/is.js b/src/plugins/data/common/es_query/kuery/functions/is.js index 4f2f298c4707d1..120dd9352d9a4f 100644 --- a/src/plugins/data/common/es_query/kuery/functions/is.js +++ b/src/plugins/data/common/es_query/kuery/functions/is.js @@ -20,7 +20,7 @@ import { get, isUndefined } from 'lodash'; import { getPhraseScript } from '../../filters'; import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; import * as ast from '../ast'; diff --git a/src/plugins/data/common/es_query/kuery/functions/range.js b/src/plugins/data/common/es_query/kuery/functions/range.js index 80181cfc003f1c..d5eba8e20253e8 100644 --- a/src/plugins/data/common/es_query/kuery/functions/range.js +++ b/src/plugins/data/common/es_query/kuery/functions/range.js @@ -22,7 +22,7 @@ import { nodeTypes } from '../node_types'; import * as ast from '../ast'; import { getRangeScript } from '../../filters'; import { getFields } from './utils/get_fields'; -import { getTimeZoneFromSettings } from '../../utils/get_time_zone_from_settings'; +import { getTimeZoneFromSettings } from '../../utils'; import { getFullFieldNameNode } from './utils/get_full_field_name_node'; export function buildNodeParams(fieldName, params) { diff --git a/src/plugins/data/common/es_query/utils/get_time_zone_from_settings.ts b/src/plugins/data/common/es_query/utils.ts similarity index 100% rename from src/plugins/data/common/es_query/utils/get_time_zone_from_settings.ts rename to src/plugins/data/common/es_query/utils.ts diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index dd445a33f21c5f..85d276767b5a79 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -194,6 +194,10 @@ export abstract class FieldFormat { [HTML_CONTEXT_TYPE]: htmlContentTypeSetup(this, this.htmlConvert), }; } + + static isInstanceOfFieldFormat(fieldFormat: any): fieldFormat is FieldFormat { + return Boolean(fieldFormat && fieldFormat.convert); + } } export type IFieldFormat = PublicMethodsOf; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index e54278698a05a8..967887764237d2 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -22,19 +22,75 @@ export function plugin(initializerContext: PluginInitializerContext) { return new DataPublicPlugin(initializerContext); } -export * from '../common'; +/** + * Types to be shared externally + * @public + */ +export { IRequestTypesMap, IResponseTypesMap } from './search'; +export * from './types'; +export { + // field formats + ContentType, // only used in agg_type + FIELD_FORMAT_IDS, + IFieldFormat, + IFieldFormatId, + IFieldFormatType, + // index patterns + IIndexPattern, + IFieldType, + IFieldSubType, + // kbn field types + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + // query + Query, + // timefilter + RefreshInterval, + TimeRange, +} from '../common'; +/** + * Static code to be shared externally + * @public + */ export * from './autocomplete_provider'; export * from './field_formats_provider'; export * from './index_patterns'; - -export * from './types'; - -export { IRequestTypesMap, IResponseTypesMap } from './search'; export * from './search'; export * from './query'; - export * from './ui'; +export { + // es query + esFilters, + esKuery, + esQuery, + // field formats + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DEFAULT_CONVERTER_COLOR, + DurationFormat, + FieldFormat, + getHighlightRequest, // only used in search source + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TEXT_CONTEXT_TYPE, // only used in agg_types + TruncateFormat, + UrlFormat, + // index patterns + isFilterable, + // kbn field types + castEsToKbnFieldTypeName, + getKbnFieldType, + getKbnTypeNames, +} from '../common'; // Export plugin after all other imports import { DataPublicPlugin } from './plugin'; diff --git a/src/plugins/data/public/index_patterns/fields/field.ts b/src/plugins/data/public/index_patterns/fields/field.ts index c8c8ac1ffd3214..6ed3c2be8f96e4 100644 --- a/src/plugins/data/public/index_patterns/fields/field.ts +++ b/src/plugins/data/public/index_patterns/fields/field.ts @@ -94,7 +94,8 @@ export class Field implements IFieldType { if (!type) type = getKbnFieldType('unknown'); let format = spec.format; - if (!format || !(format instanceof FieldFormat)) { + + if (!FieldFormat.isInstanceOfFieldFormat(format)) { const fieldFormats = getFieldFormats(); format = diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts index f56f94fa8c2605..1f83e4bd5b80c6 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.test.ts @@ -33,8 +33,6 @@ import { setNotifications, setFieldFormats } from '../../services'; import { notificationServiceMock } from '../../../../../core/public/notifications/notifications_service.mock'; import { FieldFormatRegisty } from '../../field_formats_provider'; -jest.mock('ui/new_platform'); - jest.mock('../../../../kibana_utils/public', () => { const originalModule = jest.requireActual('../../../../kibana_utils/public'); @@ -50,19 +48,6 @@ jest.mock('../../../../kibana_utils/public', () => { }; }); -jest.mock('ui/notify', () => ({ - toastNotifications: { - addDanger: jest.fn(), - addError: jest.fn(), - }, -})); - -jest.mock('ui/saved_objects', () => { - return { - findObjectByTitle: jest.fn(), - }; -}); - let mockFieldsFetcherResponse: any[] = []; jest.mock('./_fields_fetcher', () => ({ diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 058e6c0e2f5c52..03d3dad61ed052 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -74,6 +74,7 @@ const createStartContract = (): Start => { query: queryStartMock, ui: { IndexPatternSelect: jest.fn(), + SearchBar: jest.fn(), }, indexPatterns: {} as IndexPatternsContract, }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 2a37be7f3f46a1..cd55048ca527fa 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -18,7 +18,7 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { Storage } from '../../kibana_utils/public'; +import { Storage, IStorageWrapper } from '../../kibana_utils/public'; import { DataPublicPluginSetup, DataPublicPluginStart, @@ -35,24 +35,26 @@ import { IndexPatterns } from './index_patterns'; import { setNotifications, setFieldFormats, setOverlays, setIndexPatterns } from './services'; import { createFilterAction, GLOBAL_APPLY_FILTER_ACTION } from './actions'; import { APPLY_FILTER_TRIGGER } from '../../embeddable/public'; +import { createSearchBar } from './ui/search_bar/create_search_bar'; export class DataPublicPlugin implements Plugin { private readonly autocomplete = new AutocompleteProviderRegister(); private readonly searchService: SearchService; private readonly fieldFormatsService: FieldFormatsService; private readonly queryService: QueryService; + private readonly storage: IStorageWrapper; constructor(initializerContext: PluginInitializerContext) { this.searchService = new SearchService(initializerContext); this.queryService = new QueryService(); this.fieldFormatsService = new FieldFormatsService(); + this.storage = new Storage(window.localStorage); } public setup(core: CoreSetup, { uiActions }: DataSetupDependencies): DataPublicPluginSetup { - const storage = new Storage(window.localStorage); const queryService = this.queryService.setup({ uiSettings: core.uiSettings, - storage, + storage: this.storage, }); uiActions.registerAction( @@ -79,16 +81,27 @@ export class DataPublicPlugin implements Plugin; + SearchBar: React.ComponentType; }; } diff --git a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx index affbb8acecb201..92582ef1d15c22 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filter_popover_content.tsx @@ -30,7 +30,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { mapAndFlattenFilters, esFilters, utils, IIndexPattern } from '../..'; +import { mapAndFlattenFilters, esFilters, IIndexPattern } from '../..'; import { FilterLabel } from '../filter_bar'; interface Props { @@ -56,7 +56,7 @@ export class ApplyFiltersPopoverContent extends Component { }; } private getLabel(filter: esFilters.Filter) { - const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = esFilters.getDisplayValueFromFilter(filter, this.props.indexPatterns); return ; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx index 12da4cbab02da3..b058d231b83065 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/index.tsx @@ -48,7 +48,7 @@ import { Operator } from './lib/filter_operators'; import { PhraseValueInput } from './phrase_value_input'; import { PhrasesValuesInput } from './phrases_values_input'; import { RangeValueInput } from './range_value_input'; -import { esFilters, utils, IIndexPattern, IFieldType } from '../../..'; +import { esFilters, IIndexPattern, IFieldType } from '../../..'; interface Props { filter: esFilters.Filter; @@ -371,7 +371,7 @@ class FilterEditorUI extends Component { } private getIndexPatternFromFilter() { - return utils.getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); + return esFilters.getIndexPatternFromFilter(this.props.filter, this.props.indexPatterns); } private getFieldFromFilter() { diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 1921f6672755db..788663041fd038 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -20,11 +20,11 @@ import { EuiContextMenu, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; -import React, { Component } from 'react'; +import React, { Component, MouseEvent } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { FilterEditor } from './filter_editor'; import { FilterView } from './filter_view'; -import { esFilters, utils, IIndexPattern } from '../..'; +import { esFilters, IIndexPattern } from '../..'; interface Props { id: string; @@ -46,6 +46,13 @@ class FilterItemUI extends Component { isPopoverOpen: false, }; + private handleBadgeClick = (e: MouseEvent) => { + if (e.shiftKey) { + this.onToggleDisabled(); + } else { + this.togglePopover(); + } + }; public render() { const { filter, id } = this.props; const { negate, disabled } = filter.meta; @@ -60,7 +67,7 @@ class FilterItemUI extends Component { this.props.className ); - const valueLabel = utils.getDisplayValueFromFilter(filter, this.props.indexPatterns); + const valueLabel = esFilters.getDisplayValueFromFilter(filter, this.props.indexPatterns); const dataTestSubjKey = filter.meta.key ? `filter-key-${filter.meta.key}` : ''; const dataTestSubjValue = filter.meta.value ? `filter-value-${valueLabel}` : ''; const dataTestSubjDisabled = `filter-${ @@ -73,7 +80,7 @@ class FilterItemUI extends Component { valueLabel={valueLabel} className={classes} iconOnClick={() => this.props.onRemove()} - onClick={this.togglePopover} + onClick={this.handleBadgeClick} data-test-subj={`filter ${dataTestSubjDisabled} ${dataTestSubjKey} ${dataTestSubjValue}`} /> ); diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 8bfccd49bdff34..cd4ec3c3bf74b3 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -21,8 +21,8 @@ export { SuggestionsComponent } from './typeahead/suggestions_component'; export { IndexPatternSelect } from './index_pattern_select'; export { FilterBar } from './filter_bar'; export { QueryStringInput } from './query_string_input/query_string_input'; +export { SearchBar, SearchBarProps } from './search_bar'; // temp export - will be removed as final components are migrated to NP -export { QueryBarTopRow } from './query_string_input/query_bar_top_row'; export { SavedQueryManagementComponent } from './saved_query_management'; export { SaveQueryForm, SavedQueryMeta } from './saved_query_form'; diff --git a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap index 80a5ede5670543..4de883295bd8a6 100644 --- a/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap +++ b/src/plugins/data/public/ui/query_string_input/__snapshots__/query_string_input.test.tsx.snap @@ -209,6 +209,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -829,6 +830,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -1437,6 +1439,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -2054,6 +2057,7 @@ exports[`QueryStringInput Should pass the query language to the language switche }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -2662,6 +2666,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { @@ -3279,6 +3284,7 @@ exports[`QueryStringInput Should render the given query 1`] = ` }, "ui": Object { "IndexPatternSelect": [MockFunction], + "SearchBar": [MockFunction], }, }, "docLinks": Object { diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx similarity index 82% rename from src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx rename to src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 125c6b8dad006b..6f1be2825dd012 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -21,29 +21,29 @@ import React, { useState, useEffect } from 'react'; import { Subscription } from 'rxjs'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SearchBar } from '../../../'; -import { SearchBarOwnProps } from '.'; -import { DataPublicPluginStart, esFilters } from '../../../../../../../plugins/data/public'; +import { KibanaContextProvider } from '../../../../kibana_react/public'; +import { DataPublicPluginStart, esFilters } from '../..'; +import { QueryStart } from '../../query'; +import { SearchBarOwnProps, SearchBar } from './search_bar'; interface StatefulSearchBarDeps { core: CoreStart; - data: DataPublicPluginStart; + data: Omit; storage: IStorageWrapper; } -export type StatetfulSearchBarProps = SearchBarOwnProps & { +export type StatefulSearchBarProps = SearchBarOwnProps & { appName: string; }; -const defaultFiltersUpdated = (data: DataPublicPluginStart) => { +const defaultFiltersUpdated = (query: QueryStart) => { return (filters: esFilters.Filter[]) => { - data.query.filterManager.setFilters(filters); + query.filterManager.setFilters(filters); }; }; -const defaultOnRefreshChange = (data: DataPublicPluginStart) => { - const { timefilter } = data.query.timefilter; +const defaultOnRefreshChange = (query: QueryStart) => { + const { timefilter } = query.timefilter; return (options: { isPaused: boolean; refreshInterval: number }) => { timefilter.setRefreshInterval({ value: options.refreshInterval, @@ -55,7 +55,7 @@ const defaultOnRefreshChange = (data: DataPublicPluginStart) => { export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. - return (props: StatetfulSearchBarProps) => { + return (props: StatefulSearchBarProps) => { const { filterManager, timefilter } = data.query; const tfRefreshInterval = timefilter.timefilter.getRefreshInterval(); const fmFilters = filterManager.getFilters(); @@ -98,7 +98,7 @@ export function createSearchBar({ core, storage, data }: StatefulSearchBarDeps) isSubscribed = false; subscriptions.unsubscribe(); }; - }, []); + }, [filterManager, timefilter.timefilter]); return ( diff --git a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx similarity index 93% rename from src/legacy/core_plugins/data/public/search/search_bar/index.tsx rename to src/plugins/data/public/ui/search_bar/index.tsx index faf6e24aa6ed5d..4aa7f5fe2b0400 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -17,4 +17,4 @@ * under the License. */ -export * from './components'; +export { SearchBar, SearchBarProps } from './search_bar'; diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx similarity index 96% rename from src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx rename to src/plugins/data/public/ui/search_bar/search_bar.test.tsx index 5752d6a5022253..56d444761153f6 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -19,15 +19,15 @@ import React from 'react'; import { SearchBar } from './search_bar'; -import { IndexPattern } from '../../../index_patterns'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; -import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); import { mount } from 'enzyme'; +import { IIndexPattern } from '../..'; const mockTimeHistory = { get: () => { @@ -35,9 +35,14 @@ const mockTimeHistory = { }, }; -jest.mock('../../../../../../../plugins/data/public', () => { +jest.mock('../..', () => { return { FilterBar: () =>
, + }; +}); + +jest.mock('../query_string_input/query_bar_top_row', () => { + return { QueryBarTopRow: () =>
, }; }); @@ -74,7 +79,7 @@ const mockIndexPattern = { searchable: true, }, ], -} as IndexPattern; +} as IIndexPattern; const kqlQuery = { query: 'response:200', diff --git a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx similarity index 98% rename from src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx rename to src/plugins/data/public/ui/search_bar/search_bar.tsx index f547fada4a3b13..ceaeb24e7fe7c1 100644 --- a/src/legacy/core_plugins/data/public/search/search_bar/components/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -24,10 +24,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { - withKibana, - KibanaReactContextValue, -} from '../../../../../../../plugins/kibana_react/public'; +import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; import { IDataPluginServices, TimeRange, @@ -37,12 +34,12 @@ import { TimeHistoryContract, FilterBar, SavedQuery, - SavedQueryAttributes, SavedQueryMeta, SaveQueryForm, SavedQueryManagementComponent, - QueryBarTopRow, -} from '../../../../../../../plugins/data/public'; + SavedQueryAttributes, +} from '../..'; +import { QueryBarTopRow } from '../query_string_input/query_bar_top_row'; interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 81906a63bd49dd..022eb0ae502958 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -24,14 +24,70 @@ export function plugin(initializerContext: PluginInitializerContext) { return new DataServerPlugin(initializerContext); } -export { DataServerPlugin as Plugin }; +/** + * Types to be shared externally + * @public + */ +export { IRequestTypesMap, IResponseTypesMap } from './search'; +export { + // field formats + FIELD_FORMAT_IDS, + IFieldFormat, + IFieldFormatId, + IFieldFormatType, + // index patterns + IIndexPattern, + IFieldType, + IFieldSubType, + // kbn field types + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + // query + Query, + // timefilter + RefreshInterval, + TimeRange, +} from '../common'; + +/** + * Static code to be shared externally + * @public + */ export { IndexPatternsFetcher, FieldDescriptor, shouldReadFieldFromDocValues, } from './index_patterns'; - export * from './search'; -export * from '../common'; +export { + // es query + esFilters, + esKuery, + esQuery, + // field formats + BoolFormat, + BytesFormat, + ColorFormat, + DateFormat, + DateNanosFormat, + DEFAULT_CONVERTER_COLOR, + DurationFormat, + FieldFormat, + IpFormat, + NumberFormat, + PercentFormat, + RelativeDateFormat, + SourceFormat, + StaticLookupFormat, + StringFormat, + TruncateFormat, + UrlFormat, + // index patterns + isFilterable, + // kbn field types + castEsToKbnFieldTypeName, + getKbnFieldType, + getKbnTypeNames, +} from '../common'; -export { IRequestTypesMap, IResponseTypesMap } from './search'; +export { DataServerPlugin as Plugin }; diff --git a/src/plugins/data/server/search/create_api.ts b/src/plugins/data/server/search/create_api.ts index 2a874869526d73..e1613103ac3998 100644 --- a/src/plugins/data/server/search/create_api.ts +++ b/src/plugins/data/server/search/create_api.ts @@ -38,7 +38,7 @@ export function createApi({ } // Give providers access to other search strategies by injecting this function const strategy = await strategyProvider(caller, api.search); - return strategy.search(request); + return strategy.search(request, options); }, }; return api; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index 7b725a47aa13bd..99ccb4dcbebabf 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { coreMock } from '../../../../../core/server/mocks'; +import { coreMock, pluginInitializerContextConfigMock } from '../../../../../core/server/mocks'; import { esSearchStrategyProvider } from './es_search_strategy'; describe('ES search strategy', () => { @@ -31,6 +31,7 @@ describe('ES search strategy', () => { }, }); const mockSearch = jest.fn(); + const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; beforeEach(() => { mockApiCaller.mockClear(); @@ -41,6 +42,7 @@ describe('ES search strategy', () => { const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch @@ -49,11 +51,12 @@ describe('ES search strategy', () => { expect(typeof esSearch.search).toBe('function'); }); - it('logs the response if `debug` is set to `true`', () => { + it('logs the response if `debug` is set to `true`', async () => { const spy = jest.spyOn(console, 'log'); const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch @@ -61,43 +64,46 @@ describe('ES search strategy', () => { expect(spy).not.toBeCalled(); - esSearch.search({ params: {}, debug: true }); + await esSearch.search({ params: {}, debug: true }); expect(spy).toBeCalled(); }); - it('calls the API caller with the params with defaults', () => { + it('calls the API caller with the params with defaults', async () => { const params = { index: 'logstash-*' }; const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch ); - esSearch.search({ params }); + await esSearch.search({ params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); expect(mockApiCaller.mock.calls[0][1]).toEqual({ ...params, + timeout: '0ms', ignoreUnavailable: true, restTotalHitsAsInt: true, }); }); - it('calls the API caller with overridden defaults', () => { - const params = { index: 'logstash-*', ignoreUnavailable: false }; + it('calls the API caller with overridden defaults', async () => { + const params = { index: 'logstash-*', ignoreUnavailable: false, timeout: '1000ms' }; const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch ); - esSearch.search({ params }); + await esSearch.search({ params }); expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toBe('search'); @@ -112,6 +118,7 @@ describe('ES search strategy', () => { const esSearch = esSearchStrategyProvider( { core: mockCoreSetup, + config$: mockConfig$, }, mockApiCaller, mockSearch diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index c5fc1d9d3a11c2..20bc964effc02c 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { first } from 'rxjs/operators'; import { APICaller } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { ES_SEARCH_STRATEGY } from '../../../common/search'; @@ -28,7 +29,9 @@ export const esSearchStrategyProvider: TSearchStrategyProvider => { return { search: async (request, options) => { + const config = await context.config$.pipe(first()).toPromise(); const params = { + timeout: `${config.elasticsearch.shardTimeout.asMilliseconds()}ms`, ignoreUnavailable: true, // Don't fail if the index/indices don't exist restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range ...request.params, diff --git a/src/plugins/data/server/search/i_search_context.ts b/src/plugins/data/server/search/i_search_context.ts index 5f2df5d8e819ec..9d9de055d994fd 100644 --- a/src/plugins/data/server/search/i_search_context.ts +++ b/src/plugins/data/server/search/i_search_context.ts @@ -16,8 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { CoreSetup } from '../../../../core/server'; + +import { Observable } from 'rxjs'; +import { CoreSetup, SharedGlobalConfig } from '../../../../core/server'; export interface ISearchContext { core: CoreSetup; + config$: Observable; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 3409a72326121f..8ca314ad7bfd8f 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -83,6 +83,11 @@ export class SearchService implements Plugin { }; api.registerSearchStrategyContext(this.initializerContext.opaqueId, 'core', () => core); + api.registerSearchStrategyContext( + this.initializerContext.opaqueId, + 'config$', + () => this.initializerContext.config.legacy.globalConfig$ + ); // ES search capabilities are written in a way that it could easily be a separate plugin, // however these two plugins are tightly coupled due to the default search strategy using diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index b3c6bb592f378c..be142f2cc74e65 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -25,7 +25,7 @@ import * as React from 'react'; import ReactDOM from 'react-dom'; import { useEffect, useRef } from 'react'; -import { AppMountContext } from 'kibana/public'; +import { AppMountContext, AppMountDeprecated } from 'kibana/public'; import { DevTool } from './plugin'; interface DevToolsWrapperProps { @@ -91,10 +91,10 @@ function DevToolsWrapper({ if (mountedTool.current) { mountedTool.current.unmountHandler(); } - const unmountHandler = await activeDevTool.mount(appMountContext, { - element, - appBasePath: '', - }); + const params = { element, appBasePath: '' }; + const unmountHandler = isAppMountDeprecated(activeDevTool.mount) + ? await activeDevTool.mount(appMountContext, params) + : await activeDevTool.mount(params); mountedTool.current = { devTool: activeDevTool, mountpoint: element, @@ -182,3 +182,8 @@ export function renderApp( return () => ReactDOM.unmountComponentAtNode(element); } + +function isAppMountDeprecated(mount: (...args: any[]) => any): mount is AppMountDeprecated { + // Mount functions with two arguments are assumed to expect deprecated `context` object. + return mount.length === 2; +} diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 4c7ea6676f9ef9..acc2da15144831 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -17,7 +17,7 @@ * under the License. */ -import { SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectAttributes } from 'src/core/public'; import { SavedObjectMetaData } from '../types'; import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from './i_embeddable'; import { ErrorEmbeddable } from './error_embeddable'; diff --git a/src/plugins/es_ui_shared/public/request/np_ready_request.ts b/src/plugins/es_ui_shared/public/request/np_ready_request.ts index 48c7904661e515..5a3f28ed76486c 100644 --- a/src/plugins/es_ui_shared/public/request/np_ready_request.ts +++ b/src/plugins/es_ui_shared/public/request/np_ready_request.ts @@ -19,11 +19,12 @@ import { useEffect, useState, useRef } from 'react'; -import { HttpServiceBase } from '../../../../../src/core/public'; +import { HttpServiceBase, HttpFetchQuery } from '../../../../../src/core/public'; export interface SendRequestConfig { path: string; method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head'; + query?: HttpFetchQuery; body?: any; } @@ -48,10 +49,10 @@ export interface UseRequestResponse { export const sendRequest = async ( httpClient: HttpServiceBase, - { path, method, body }: SendRequestConfig + { path, method, body, query }: SendRequestConfig ): Promise => { try { - const response = await httpClient[method](path, { body }); + const response = await httpClient[method](path, { body, query }); return { data: response.data ? response.data : response, @@ -70,6 +71,7 @@ export const useRequest = ( { path, method, + query, body, pollIntervalMs, initialData, @@ -112,6 +114,7 @@ export const useRequest = ( const requestBody = { path, method, + query, body, }; diff --git a/src/plugins/eui_utils/public/eui_utils.ts b/src/plugins/eui_utils/public/eui_utils.ts index 12249bf9eca90b..d9c10c34dd4a8b 100644 --- a/src/plugins/eui_utils/public/eui_utils.ts +++ b/src/plugins/eui_utils/public/eui_utils.ts @@ -42,7 +42,7 @@ export class EuiUtils { useEffect(() => { const s = getChartsTheme$().subscribe(update); return () => s.unsubscribe(); - }, [false]); + }, []); return value; }; diff --git a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx index 51fbbd2ba3046d..bd2beaf77a3059 100644 --- a/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx +++ b/src/plugins/kibana_react/public/saved_objects/saved_object_finder.tsx @@ -44,7 +44,7 @@ import { import { Direction } from '@elastic/eui/src/services/sort/sort_direction'; import { i18n } from '@kbn/i18n'; -import { SavedObjectAttributes } from '../../../../core/server'; +import { SavedObjectAttributes } from '../../../../core/public'; import { SimpleSavedObject, CoreStart } from '../../../../core/public'; import { useKibana } from '../context'; diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json index bbe393a76c5dae..dce2ac9281aba7 100644 --- a/src/plugins/share/kibana.json +++ b/src/plugins/share/kibana.json @@ -1,6 +1,6 @@ { "id": "share", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/share/public/components/share_context_menu.test.tsx b/src/plugins/share/public/components/share_context_menu.test.tsx index 7fb0449ead5020..1f2242ae4c5158 100644 --- a/src/plugins/share/public/components/share_context_menu.test.tsx +++ b/src/plugins/share/public/components/share_context_menu.test.tsx @@ -34,7 +34,7 @@ const defaultProps = { isDirty: false, onClose: () => {}, basePath: '', - post: () => Promise.resolve(), + post: () => Promise.resolve({} as any), objectType: 'dashboard', }; diff --git a/src/plugins/share/public/components/url_panel_content.test.tsx b/src/plugins/share/public/components/url_panel_content.test.tsx index 9da1a23641ab86..9db8d1ccf2efa2 100644 --- a/src/plugins/share/public/components/url_panel_content.test.tsx +++ b/src/plugins/share/public/components/url_panel_content.test.tsx @@ -28,7 +28,7 @@ const defaultProps = { allowShortUrl: true, objectType: 'dashboard', basePath: '', - post: () => Promise.resolve(), + post: () => Promise.resolve({} as any), }; test('render', () => { diff --git a/src/legacy/ui/public/ui_metric/index.ts b/src/plugins/share/server/index.ts similarity index 78% rename from src/legacy/ui/public/ui_metric/index.ts rename to src/plugins/share/server/index.ts index ad43f27201ce4d..9e574314f80000 100644 --- a/src/legacy/ui/public/ui_metric/index.ts +++ b/src/plugins/share/server/index.ts @@ -17,12 +17,9 @@ * under the License. */ -let _canTrackUiMetrics = false; +import { PluginInitializerContext } from '../../../core/server'; +import { SharePlugin } from './plugin'; -export function setCanTrackUiMetrics(flag: boolean) { - _canTrackUiMetrics = flag; -} - -export function getCanTrackUiMetrics(): boolean { - return _canTrackUiMetrics; +export function plugin(initializerContext: PluginInitializerContext) { + return new SharePlugin(initializerContext); } diff --git a/src/legacy/ui/public/utils/find_by_param.ts b/src/plugins/share/server/plugin.ts similarity index 59% rename from src/legacy/ui/public/utils/find_by_param.ts rename to src/plugins/share/server/plugin.ts index de32fc955a8cd3..bcb681a50652a8 100644 --- a/src/legacy/ui/public/utils/find_by_param.ts +++ b/src/plugins/share/server/plugin.ts @@ -17,22 +17,21 @@ * under the License. */ -import _ from 'lodash'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; +import { createRoutes } from './routes/create_routes'; -interface AnyObject { - [key: string]: any; -} +export class SharePlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public async setup(core: CoreSetup) { + createRoutes(core, this.initializerContext.logger.get()); + } + + public start() { + this.initializerContext.logger.get().debug('Starting plugin'); + } -// given an object or array of objects, return the value of the passed param -// if the param is missing, return undefined -export function findByParam(values: AnyObject | AnyObject[], param: string) { - if (Array.isArray(values)) { - // point series chart - const index = _.findIndex(values, param); - if (index === -1) { - return; - } - return values[index][param]; + public stop() { + this.initializerContext.logger.get().debug('Stopping plugin'); } - return values[param]; // pie chart } diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts new file mode 100644 index 00000000000000..bd4b6fdb08791d --- /dev/null +++ b/src/plugins/share/server/routes/create_routes.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Logger } from 'kibana/server'; + +import { shortUrlLookupProvider } from './lib/short_url_lookup'; +import { createGotoRoute } from './goto'; +import { createShortenUrlRoute } from './shorten_url'; + +export function createRoutes({ http }: CoreSetup, logger: Logger) { + const shortUrlLookup = shortUrlLookupProvider({ logger }); + const router = http.createRouter(); + + createGotoRoute({ router, shortUrlLookup, http }); + createShortenUrlRoute({ router, shortUrlLookup }); +} diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts new file mode 100644 index 00000000000000..7343dc1bd34a26 --- /dev/null +++ b/src/plugins/share/server/routes/goto.ts @@ -0,0 +1,64 @@ +/* + * 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, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; + +export const createGotoRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: '/goto/{urlId}', + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); + shortUrlAssertValid(url); + + const uiSettings = context.core.uiSettings.client; + const stateStoreInSessionStorage = await uiSettings.get('state:storeInSessionStorage'); + if (!stateStoreInSessionStorage) { + return response.redirected({ + headers: { + location: http.basePath.prepend(url), + }, + }); + } + return response.redirected({ + headers: { + location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + }, + }); + }) + ); +}; diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts new file mode 100644 index 00000000000000..f83073e6aefe90 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.test.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shortUrlAssertValid } from './short_url_assert_valid'; + +describe('shortUrlAssertValid()', () => { + const invalid = [ + ['protocol', 'http://localhost:5601/app/kibana'], + ['protocol', 'https://localhost:5601/app/kibana'], + ['protocol', 'mailto:foo@bar.net'], + ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url + ['hostname', 'localhost/app/kibana'], + ['hostname and port', 'local.host:5601/app/kibana'], + ['hostname and auth', 'user:pass@localhost.net/app/kibana'], + ['path traversal', '/app/../../not-kibana'], + ['deep path', '/app/kibana/foo'], + ['deep path', '/app/kibana/foo/bar'], + ['base path', '/base/app/kibana'], + ]; + + invalid.forEach(([desc, url]) => { + it(`fails when url has ${desc}`, () => { + try { + shortUrlAssertValid(url); + throw new Error(`expected assertion to throw`); + } catch (err) { + if (!err || !err.isBoom) { + throw err; + } + } + }); + }); + + const valid = [ + '/app/kibana', + '/app/monitoring#angular/route', + '/app/text#document-id', + '/app/some?with=query', + '/app/some?with=query#and-a-hash', + ]; + + valid.forEach(url => { + it(`allows ${url}`, () => { + shortUrlAssertValid(url); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_assert_valid.ts b/src/plugins/share/server/routes/lib/short_url_assert_valid.ts new file mode 100644 index 00000000000000..2f120bbc03cd73 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_assert_valid.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 { parse } from 'url'; +import { trim } from 'lodash'; +import Boom from 'boom'; + +export function shortUrlAssertValid(url: string) { + const { protocol, hostname, pathname } = parse(url); + + if (protocol) { + throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); + } + + if (hostname) { + throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); + } + + const pathnameParts = trim(pathname, '/').split('/'); + if (pathnameParts.length !== 2) { + throw Boom.notAcceptable( + `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` + ); + } +} diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.test.ts b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts new file mode 100644 index 00000000000000..87e2b7b726e599 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { shortUrlLookupProvider, ShortUrlLookupService } from './short_url_lookup'; +import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { SavedObjectsClient } from '../../../../../core/server'; + +describe('shortUrlLookupProvider', () => { + const ID = 'bf00ad16941fc51420f91a93428b27a0'; + const TYPE = 'url'; + const URL = 'http://elastic.co'; + + let savedObjects: jest.Mocked; + let deps: { savedObjects: SavedObjectsClientContract }; + let shortUrl: ShortUrlLookupService; + + beforeEach(() => { + savedObjects = ({ + get: jest.fn(), + create: jest.fn(() => Promise.resolve({ id: ID })), + update: jest.fn(), + errors: SavedObjectsClient.errors, + } as unknown) as jest.Mocked; + + deps = { savedObjects }; + shortUrl = shortUrlLookupProvider({ logger: ({ warn: () => {} } as unknown) as Logger }); + }); + + describe('generateUrlId', () => { + it('returns the document id', async () => { + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + + it('provides correct arguments to savedObjectsClient', async () => { + await shortUrl.generateUrlId(URL, { savedObjects }); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes, options] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + expect(options!.id).toEqual(ID); + }); + + it('passes persists attributes', async () => { + await shortUrl.generateUrlId(URL, deps); + + expect(savedObjects.create).toHaveBeenCalledTimes(1); + const [type, attributes] = savedObjects.create.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(Object.keys(attributes).sort()).toEqual([ + 'accessCount', + 'accessDate', + 'createDate', + 'url', + ]); + expect(attributes.url).toEqual(URL); + }); + + it('gracefully handles version conflict', async () => { + const error = savedObjects.errors.decorateConflictError(new Error()); + savedObjects.create.mockImplementation(() => { + throw error; + }); + const id = await shortUrl.generateUrlId(URL, deps); + expect(id).toEqual(ID); + }); + }); + + describe('getUrl', () => { + beforeEach(() => { + const attributes = { accessCount: 2, url: URL }; + savedObjects.get.mockResolvedValue({ id: ID, attributes, type: 'url', references: [] }); + }); + + it('provides the ID to savedObjectsClient', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.get).toHaveBeenCalledTimes(1); + expect(savedObjects.get).toHaveBeenCalledWith(TYPE, ID); + }); + + it('returns the url', async () => { + const response = await shortUrl.getUrl(ID, deps); + expect(response).toEqual(URL); + }); + + it('increments accessCount', async () => { + await shortUrl.getUrl(ID, { savedObjects }); + + expect(savedObjects.update).toHaveBeenCalledTimes(1); + + const [type, id, attributes] = savedObjects.update.mock.calls[0]; + + expect(type).toEqual(TYPE); + expect(id).toEqual(ID); + expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); + expect(attributes.accessCount).toEqual(3); + }); + }); +}); diff --git a/src/plugins/share/server/routes/lib/short_url_lookup.ts b/src/plugins/share/server/routes/lib/short_url_lookup.ts new file mode 100644 index 00000000000000..0d8a9c86621de3 --- /dev/null +++ b/src/plugins/share/server/routes/lib/short_url_lookup.ts @@ -0,0 +1,84 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import crypto from 'crypto'; +import { get } from 'lodash'; + +import { Logger, SavedObject, SavedObjectsClientContract } from 'kibana/server'; + +export interface ShortUrlLookupService { + generateUrlId(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; + getUrl(url: string, deps: { savedObjects: SavedObjectsClientContract }): Promise; +} + +export function shortUrlLookupProvider({ logger }: { logger: Logger }): ShortUrlLookupService { + async function updateMetadata( + doc: SavedObject, + { savedObjects }: { savedObjects: SavedObjectsClientContract } + ) { + try { + await savedObjects.update('url', doc.id, { + accessDate: new Date().valueOf(), + accessCount: get(doc, 'attributes.accessCount', 0) + 1, + }); + } catch (error) { + logger.warn('Warning: Error updating url metadata'); + logger.warn(error); + // swallow errors. It isn't critical if there is no update. + } + } + + return { + async generateUrlId(url, { savedObjects }) { + const id = crypto + .createHash('md5') + .update(url) + .digest('hex'); + const { isConflictError } = savedObjects.errors; + + try { + const doc = await savedObjects.create( + 'url', + { + url, + accessCount: 0, + createDate: new Date().valueOf(), + accessDate: new Date().valueOf(), + }, + { id } + ); + + return doc.id; + } catch (error) { + if (isConflictError(error)) { + return id; + } + + throw error; + } + }, + + async getUrl(id, { savedObjects }) { + const doc = await savedObjects.get('url', id); + updateMetadata(doc, { savedObjects }); + + return doc.attributes.url; + }, + }; +} diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts new file mode 100644 index 00000000000000..116b90c6971c5c --- /dev/null +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -0,0 +1,48 @@ +/* + * 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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; + +export const createShortenUrlRoute = ({ + shortUrlLookup, + router, +}: { + shortUrlLookup: ShortUrlLookupService; + router: IRouter; +}) => { + router.post( + { + path: '/api/shorten_url', + validate: { + body: schema.object({ url: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + shortUrlAssertValid(request.body.url); + const urlId = await shortUrlLookup.generateUrlId(request.body.url, { + savedObjects: context.core.savedObjects.client, + }); + return response.ok({ body: { urlId } }); + }) + ); +}; diff --git a/src/plugins/testbed/server/index.ts b/src/plugins/testbed/server/index.ts index 07fda4eb98727e..1b4de8f8f5c953 100644 --- a/src/plugins/testbed/server/index.ts +++ b/src/plugins/testbed/server/index.ts @@ -41,6 +41,11 @@ export const config: PluginConfigDescriptor = { uiProp: true, }, schema: configSchema, + deprecations: ({ rename, unused, renameFromRoot }) => [ + rename('securityKey', 'secret'), + renameFromRoot('oldtestbed.uiProp', 'testbed.uiProp'), + unused('deprecatedProperty'), + ], }; class Plugin { diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 4502e1a6ceacfe..4b81fe9b22083a 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -137,3 +137,83 @@ There are a few ways you can test that your usage collector is working properly. Keep it simple, and keep it to a model that Kibana will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine. 2. **If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?** Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that. + + +# UI Metric app + +## Purpose + +The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with +various UIs within Kibana. It's useful for gathering _aggregate_ information, e.g. "How many times +has Button X been clicked" or "How many times has Page Y been viewed". + +With some finagling, it's even possible to add more meaning to the info you gather, such as "How many +visualizations were created in less than 5 minutes". + +### What it doesn't do + +The UI Metric app doesn't gather any metadata around a user interaction, e.g. the user's identity, +the name of a dashboard they've viewed, or the timestamp of the interaction. + +## How to use it + +To track a user interaction, import the `createUiStatsReporter` helper function from UI Metric app: + +```js +import { createUiStatsReporter, METRIC_TYPE } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; +const trackMetric = createUiStatsReporter(``); +trackMetric(METRIC_TYPE.CLICK, ``); +trackMetric('click', ``); +``` + +Metric Types: + - `METRIC_TYPE.CLICK` for tracking clicks `trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');` + - `METRIC_TYPE.LOADED` for a component load or page load `trackMetric(METRIC_TYPE.LOADED', 'my_component_loaded');` + - `METRIC_TYPE.COUNT` for a tracking a misc count `trackMetric(METRIC_TYPE.COUNT', 'my_counter', });` + +Call this function whenever you would like to track a user interaction within your app. The function +accepts two arguments, `metricType` and `eventNames`. These should be underscore-delimited strings. +For example, to track the `my_event` metric in the app `my_app` call `trackUiMetric(METRIC_TYPE.*, 'my_event)`. + +That's all you need to do! + +To track multiple metrics within a single request, provide an array of events, e.g. `trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3'])`. + +### Disallowed characters + +The colon character (`:`) should not be used in app name or event names. Colons play +a special role in how metrics are stored as saved objects. + +### Tracking timed interactions + +If you want to track how long it takes a user to do something, you'll need to implement the timing +logic yourself. You'll also need to predefine some buckets into which the UI metric can fall. +For example, if you're timing how long it takes to create a visualization, you may decide to +measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes. +To track these interactions, you'd use the timed length of the interaction to determine whether to +use a `eventName` of `create_vis_1m`, `create_vis_5m`, `create_vis_20m`, or `create_vis_infinity`. + +## How it works + +Under the hood, your app and metric type will be stored in a saved object of type `user-metric` and the +ID `ui-metric:my_app:my_metric`. This saved object will have a `count` property which will be incremented +every time the above URI is hit. + +These saved objects are automatically consumed by the stats API and surfaced under the +`ui_metric` namespace. + +```json +{ + "ui_metric": { + "my_app": [ + { + "key": "my_metric", + "value": 3 + } + ] + } +} +``` + +By storing these metrics and their counts as key-value pairs, we can add more metrics without having +to worry about exceeding the 1000-field soft limit in Elasticsearch. \ No newline at end of file diff --git a/src/plugins/usage_collection/common/constants.ts b/src/plugins/usage_collection/common/constants.ts index edd06b171a72c8..96b24c5e7475ed 100644 --- a/src/plugins/usage_collection/common/constants.ts +++ b/src/plugins/usage_collection/common/constants.ts @@ -18,3 +18,4 @@ */ export const KIBANA_STATS_TYPE = 'kibana_stats'; +export const DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S = 60; diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json index 145cd89ff884d8..ae86b6c5d7ad18 100644 --- a/src/plugins/usage_collection/kibana.json +++ b/src/plugins/usage_collection/kibana.json @@ -3,5 +3,5 @@ "configPath": ["usageCollection"], "version": "kibana", "server": true, - "ui": false + "ui": true } diff --git a/src/legacy/ui/public/saved_objects/saved_object.d.ts b/src/plugins/usage_collection/public/index.ts similarity index 70% rename from src/legacy/ui/public/saved_objects/saved_object.d.ts rename to src/plugins/usage_collection/public/index.ts index dc6496eacfcbed..712e6a76152a26 100644 --- a/src/legacy/ui/public/saved_objects/saved_object.d.ts +++ b/src/plugins/usage_collection/public/index.ts @@ -17,14 +17,12 @@ * under the License. */ -export interface SaveOptions { - confirmOverwrite: boolean; - isTitleDuplicateConfirmed: boolean; - onTitleDuplicate: () => void; -} +import { PluginInitializerContext } from '../../../core/public'; +import { UsageCollectionPlugin } from './plugin'; + +export { METRIC_TYPE } from '@kbn/analytics'; +export { UsageCollectionSetup } from './plugin'; -export interface SavedObject { - save: (saveOptions: SaveOptions) => Promise; - copyOnSave: boolean; - id?: string; +export function plugin(initializerContext: PluginInitializerContext) { + return new UsageCollectionPlugin(initializerContext); } diff --git a/src/legacy/ui/public/utils/date_range.ts b/src/plugins/usage_collection/public/mocks.ts similarity index 68% rename from src/legacy/ui/public/utils/date_range.ts rename to src/plugins/usage_collection/public/mocks.ts index ca44183b8d68be..69fbf56ca5604d 100644 --- a/src/legacy/ui/public/utils/date_range.ts +++ b/src/plugins/usage_collection/public/mocks.ts @@ -17,16 +17,20 @@ * under the License. */ -import { DateRangeKey } from '../agg_types/buckets/date_range'; +import { UsageCollectionSetup, METRIC_TYPE } from '.'; -export const dateRange = { - toString({ from, to }: DateRangeKey, format: (val: any) => string) { - if (!from) { - return 'Before ' + format(to); - } else if (!to) { - return 'After ' + format(from); - } else { - return format(from) + ' to ' + format(to); - } - }, +export type Setup = jest.Mocked; + +const createSetupContract = (): Setup => { + const setupContract: Setup = { + allowTrackUserAgent: jest.fn(), + reportUiStats: jest.fn(), + METRIC_TYPE, + }; + + return setupContract; +}; + +export const usageCollectionPluginMock = { + createSetupContract, }; diff --git a/src/plugins/usage_collection/public/plugin.ts b/src/plugins/usage_collection/public/plugin.ts new file mode 100644 index 00000000000000..2ecc6c8bc2038f --- /dev/null +++ b/src/plugins/usage_collection/public/plugin.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 { Reporter, METRIC_TYPE } from '@kbn/analytics'; +import { Storage } from '../../kibana_utils/public'; +import { createReporter } from './services'; +import { + PluginInitializerContext, + Plugin, + CoreSetup, + CoreStart, + HttpServiceBase, +} from '../../../core/public'; + +interface PublicConfigType { + uiMetric: { + enabled: boolean; + debug: boolean; + }; +} + +export interface UsageCollectionSetup { + allowTrackUserAgent: (allow: boolean) => void; + reportUiStats: Reporter['reportUiStats']; + METRIC_TYPE: typeof METRIC_TYPE; +} + +export function isUnauthenticated(http: HttpServiceBase) { + const { anonymousPaths } = http; + return anonymousPaths.isAnonymous(window.location.pathname); +} + +export class UsageCollectionPlugin implements Plugin { + private trackUserAgent: boolean = true; + private reporter?: Reporter; + private config: PublicConfigType; + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } + + public setup({ http }: CoreSetup): UsageCollectionSetup { + const localStorage = new Storage(window.localStorage); + const debug = this.config.uiMetric.debug; + + this.reporter = createReporter({ + localStorage, + debug, + fetch: http, + }); + + return { + allowTrackUserAgent: (allow: boolean) => { + this.trackUserAgent = allow; + }, + reportUiStats: this.reporter.reportUiStats, + METRIC_TYPE, + }; + } + + public start({ http }: CoreStart) { + if (!this.reporter) { + return; + } + + if (this.config.uiMetric.enabled && !isUnauthenticated(http)) { + this.reporter.start(); + } + + if (this.trackUserAgent) { + this.reporter.reportUserAgent('kibana'); + } + } + + public stop() {} +} diff --git a/src/plugins/usage_collection/public/services/create_reporter.ts b/src/plugins/usage_collection/public/services/create_reporter.ts new file mode 100644 index 00000000000000..6bc35de8972c32 --- /dev/null +++ b/src/plugins/usage_collection/public/services/create_reporter.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Reporter, Storage } from '@kbn/analytics'; +import { HttpServiceBase } from 'kibana/public'; + +interface AnalyicsReporterConfig { + localStorage: Storage; + debug: boolean; + fetch: HttpServiceBase; +} + +export function createReporter(config: AnalyicsReporterConfig): Reporter { + const { localStorage, debug, fetch } = config; + + return new Reporter({ + debug, + storage: localStorage, + async http(report) { + const response = await fetch.post('/api/ui_metric/report', { + body: JSON.stringify({ report }), + }); + + if (response.status !== 'ok') { + throw Error('Unable to store report.'); + } + return response; + }, + }); +} diff --git a/src/legacy/core_plugins/ui_metric/common/index.ts b/src/plugins/usage_collection/public/services/index.ts similarity index 93% rename from src/legacy/core_plugins/ui_metric/common/index.ts rename to src/plugins/usage_collection/public/services/index.ts index 02aa55c30965d2..7703d5cc3bfeb1 100644 --- a/src/legacy/core_plugins/ui_metric/common/index.ts +++ b/src/plugins/usage_collection/public/services/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export const API_BASE_PATH = '/api/ui_metric'; +export { createReporter } from './create_reporter'; diff --git a/src/plugins/usage_collection/server/config.ts b/src/plugins/usage_collection/server/config.ts index 987db1f2b0ff3c..76379d9385cff7 100644 --- a/src/plugins/usage_collection/server/config.ts +++ b/src/plugins/usage_collection/server/config.ts @@ -17,8 +17,29 @@ * under the License. */ -import { schema } from '@kbn/config-schema'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; +import { DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S } from '../common/constants'; -export const ConfigSchema = schema.object({ - maximumWaitTimeForAllCollectorsInS: schema.number({ defaultValue: 60 }), +export const configSchema = schema.object({ + uiMetric: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + debug: schema.boolean({ defaultValue: schema.contextRef('dev') }), + }), + maximumWaitTimeForAllCollectorsInS: schema.number({ + defaultValue: DEFAULT_MAXIMUM_WAIT_TIME_FOR_ALL_COLLECTORS_IN_S, + }), }); + +export type ConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('ui_metric.enabled', 'usageCollection.uiMetric.enabled'), + renameFromRoot('ui_metric.debug', 'usageCollection.uiMetric.debug'), + ], + exposeToBrowser: { + uiMetric: true, + }, +}; diff --git a/src/plugins/usage_collection/server/index.ts b/src/plugins/usage_collection/server/index.ts index 33a1a0adc67135..6a28dba50a9151 100644 --- a/src/plugins/usage_collection/server/index.ts +++ b/src/plugins/usage_collection/server/index.ts @@ -18,10 +18,9 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; -import { ConfigSchema } from './config'; +import { UsageCollectionPlugin } from './plugin'; export { UsageCollectionSetup } from './plugin'; -export const config = { schema: ConfigSchema }; +export { config } from './config'; export const plugin = (initializerContext: PluginInitializerContext) => - new Plugin(initializerContext); + new UsageCollectionPlugin(initializerContext); diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index e8bbc8e512a41d..5c5b58ae849360 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -18,22 +18,25 @@ */ import { first } from 'rxjs/operators'; -import { TypeOf } from '@kbn/config-schema'; -import { ConfigSchema } from './config'; -import { PluginInitializerContext, Logger } from '../../../../src/core/server'; +import { ConfigType } from './config'; +import { PluginInitializerContext, Logger, CoreSetup } from '../../../../src/core/server'; import { CollectorSet } from './collector'; +import { setupRoutes } from './routes'; -export type UsageCollectionSetup = CollectorSet; +export type UsageCollectionSetup = CollectorSet & { + registerLegacySavedObjects: (legacySavedObjects: any) => void; +}; -export class Plugin { +export class UsageCollectionPlugin { logger: Logger; + private legacySavedObjects: any; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); } - public async setup(): Promise { + public async setup(core: CoreSetup) { const config = await this.initializerContext.config - .create>() + .create() .pipe(first()) .toPromise(); @@ -42,7 +45,16 @@ export class Plugin { maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); - return collectorSet; + const router = core.http.createRouter(); + const getLegacySavedObjects = () => this.legacySavedObjects; + setupRoutes(router, getLegacySavedObjects); + + return { + ...collectorSet, + registerLegacySavedObjects: (legacySavedObjects: any) => { + this.legacySavedObjects = legacySavedObjects; + }, + }; } public start() { diff --git a/src/plugins/data/common/es_query/utils/index.ts b/src/plugins/usage_collection/server/report/index.ts similarity index 85% rename from src/plugins/data/common/es_query/utils/index.ts rename to src/plugins/usage_collection/server/report/index.ts index 79856c9e0267ee..bb8c1d149fdd29 100644 --- a/src/plugins/data/common/es_query/utils/index.ts +++ b/src/plugins/usage_collection/server/report/index.ts @@ -17,6 +17,5 @@ * under the License. */ -export * from './get_time_zone_from_settings'; -export * from './get_index_pattern_from_filter'; -export * from './get_display_value'; +export { storeReport } from './store_report'; +export { reportSchema } from './schema'; diff --git a/src/plugins/usage_collection/server/report/schema.ts b/src/plugins/usage_collection/server/report/schema.ts new file mode 100644 index 00000000000000..5adf7d6575a707 --- /dev/null +++ b/src/plugins/usage_collection/server/report/schema.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { METRIC_TYPE } from '@kbn/analytics'; + +export const reportSchema = schema.object({ + reportVersion: schema.maybe(schema.literal(1)), + userAgent: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + key: schema.string(), + type: schema.string(), + appName: schema.string(), + userAgent: schema.string(), + }) + ) + ), + uiStatsMetrics: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + key: schema.string(), + type: schema.oneOf([ + schema.literal(METRIC_TYPE.CLICK), + schema.literal(METRIC_TYPE.LOADED), + schema.literal(METRIC_TYPE.COUNT), + ]), + appName: schema.string(), + eventName: schema.string(), + stats: schema.object({ + min: schema.number(), + sum: schema.number(), + max: schema.number(), + avg: schema.number(), + }), + }) + ) + ), +}); + +export type ReportSchemaType = TypeOf; diff --git a/src/plugins/usage_collection/server/report/store_report.ts b/src/plugins/usage_collection/server/report/store_report.ts new file mode 100644 index 00000000000000..9232a23d6151b6 --- /dev/null +++ b/src/plugins/usage_collection/server/report/store_report.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 { ReportSchemaType } from './schema'; + +export async function storeReport(internalRepository: any, report: ReportSchemaType) { + const uiStatsMetrics = report.uiStatsMetrics ? Object.entries(report.uiStatsMetrics) : []; + const userAgents = report.userAgent ? Object.entries(report.userAgent) : []; + return Promise.all([ + ...userAgents.map(async ([key, metric]) => { + const { userAgent } = metric; + const savedObjectId = `${key}:${userAgent}`; + return await internalRepository.create( + 'ui-metric', + { count: 1 }, + { + id: savedObjectId, + overwrite: true, + } + ); + }), + ...uiStatsMetrics.map(async ([key, metric]) => { + const { appName, eventName } = metric; + const savedObjectId = `${appName}:${eventName}`; + return await internalRepository.incrementCounter('ui-metric', savedObjectId, 'count'); + }), + ]); +} diff --git a/src/legacy/ui/public/utils/range.d.ts b/src/plugins/usage_collection/server/routes/index.ts similarity index 76% rename from src/legacy/ui/public/utils/range.d.ts rename to src/plugins/usage_collection/server/routes/index.ts index c484c6f43eebba..9e0d74add57bdc 100644 --- a/src/legacy/ui/public/utils/range.d.ts +++ b/src/plugins/usage_collection/server/routes/index.ts @@ -17,12 +17,9 @@ * under the License. */ -export function parseRange(input: string): Range; +import { IRouter } from '../../../../../src/core/server'; +import { registerUiMetricRoute } from './report_metrics'; -export interface Range { - min: number; - max: number; - minInclusive: boolean; - maxInclusive: boolean; - within(n: number): boolean; +export function setupRoutes(router: IRouter, getLegacySavedObjects: any) { + registerUiMetricRoute(router, getLegacySavedObjects); } diff --git a/src/plugins/usage_collection/server/routes/report_metrics.ts b/src/plugins/usage_collection/server/routes/report_metrics.ts new file mode 100644 index 00000000000000..93f03ea8067d21 --- /dev/null +++ b/src/plugins/usage_collection/server/routes/report_metrics.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { storeReport, reportSchema } from '../report'; + +export function registerUiMetricRoute(router: IRouter, getLegacySavedObjects: () => any) { + router.post( + { + path: '/api/ui_metric/report', + validate: { + body: schema.object({ + report: reportSchema, + }), + }, + }, + async (context, req, res) => { + const { report } = req.body; + try { + const internalRepository = getLegacySavedObjects(); + await storeReport(internalRepository, report); + return res.ok({ body: { status: 'ok' } }); + } catch (error) { + return res.ok({ body: { status: 'fail' } }); + } + } + ); +} diff --git a/src/test_utils/kbn_server.ts b/src/test_utils/kbn_server.ts index 0fc4c1f0d352ea..40700e05bcde89 100644 --- a/src/test_utils/kbn_server.ts +++ b/src/test_utils/kbn_server.ts @@ -33,7 +33,6 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; import { CliArgs, Env } from '../core/server/config'; -import { LegacyObjectToConfigAdapter } from '../core/server/legacy'; import { Root } from '../core/server/root'; import KbnServer from '../legacy/server/kbn_server'; import { CallCluster } from '../legacy/core_plugins/elasticsearch'; @@ -84,9 +83,9 @@ export function createRootWithSettings( }); return new Root( - new BehaviorSubject( - new LegacyObjectToConfigAdapter(defaultsDeep({}, settings, DEFAULTS_SETTINGS)) - ), + { + getConfig$: () => new BehaviorSubject(defaultsDeep({}, settings, DEFAULTS_SETTINGS)), + }, env ); } diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 25723677390bdf..23371e52dd9e1c 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -17,8 +17,9 @@ * under the License. */ -import { resolve, dirname } from 'path'; +import { dirname } from 'path'; import { times } from 'lodash'; +import { makeJunitReportPath } from '@kbn/test'; const TOTAL_CI_SHARDS = 4; const ROOT = dirname(require.resolve('../../package.json')); @@ -79,7 +80,7 @@ module.exports = function (grunt) { reporters: pickReporters(), junitReporter: { - outputFile: resolve(ROOT, 'target/junit', process.env.JOB || '.', `TEST-${process.env.JOB ? process.env.JOB + '-' : ''}karma.xml`), + outputFile: makeJunitReportPath(ROOT, 'karma'), useBrowserName: false, nameFormatter: (_, result) => [...result.suite, result.description].join(' '), classNameFormatter: (_, result) => { diff --git a/test/accessibility/apps/console.ts b/test/accessibility/apps/console.ts new file mode 100644 index 00000000000000..5d9bcfb73117f1 --- /dev/null +++ b/test/accessibility/apps/console.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'console']); + const a11y = getService('a11y'); + + describe('Dev tools console', () => { + before(async () => { + await PageObjects.common.navigateToApp('console'); + }); + + it('Dev tools console view', async () => { + await a11y.testAppSnapshot(); + }); + + it('Dev tools settings page', async () => { + await PageObjects.console.setFontSizeSetting(20); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/apps/dashboard.ts b/test/accessibility/apps/dashboard.ts new file mode 100644 index 00000000000000..9988e0f72ad3de --- /dev/null +++ b/test/accessibility/apps/dashboard.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header']); + const a11y = getService('a11y'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + describe('Dashboard', () => { + const dashboardName = 'Dashboard Listing A11y'; + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.update({ + defaultIndex: 'logstash-*', + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('dashboard', async () => { + await a11y.testAppSnapshot(); + }); + + it('create dashboard button', async () => { + await PageObjects.dashboard.clickCreateDashboardPrompt(); + await a11y.testAppSnapshot(); + }); + + it('save empty dashboard', async () => { + await PageObjects.dashboard.saveDashboard(dashboardName); + await a11y.testAppSnapshot(); + }); + + it('Dashboard listing table', async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/apps/home.ts b/test/accessibility/apps/home.ts index 4df4d5f42c93d8..3a67a6fb175b64 100644 --- a/test/accessibility/apps/home.ts +++ b/test/accessibility/apps/home.ts @@ -20,7 +20,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common']); + const PageObjects = getPageObjects(['common', 'home']); const a11y = getService('a11y'); describe('Kibana Home', () => { @@ -31,5 +31,15 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('Kibana Home view', async () => { await a11y.testAppSnapshot(); }); + + it('Add Kibana sample data page', async () => { + await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); + await a11y.testAppSnapshot(); + }); + + it('Add flights sample data set', async () => { + await PageObjects.home.addSampleDataSet('flights'); + await a11y.testAppSnapshot(); + }); }); } diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index 842f4ecbafa9ef..99afb21632ffac 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -21,6 +21,8 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings']); + + const testSubjects = getService('testSubjects'); const a11y = getService('a11y'); describe('Management', () => { @@ -42,6 +44,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); + it('Create Index pattern wizard', async () => { + await PageObjects.settings.clickKibanaIndexPatterns(); + await (await testSubjects.find('createIndexPatternButton')).click(); + await a11y.testAppSnapshot(); + }); + it('Saved objects view', async () => { await PageObjects.settings.clickKibanaSavedObjects(); await a11y.testAppSnapshot(); diff --git a/test/accessibility/apps/visualize.ts b/test/accessibility/apps/visualize.ts new file mode 100644 index 00000000000000..4a68640aae2825 --- /dev/null +++ b/test/accessibility/apps/visualize.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'visualize', 'header']); + const a11y = getService('a11y'); + + describe('Visualize', () => { + before(async () => { + await PageObjects.common.navigateToApp('visualize'); + }); + + it('visualize', async () => { + await a11y.testAppSnapshot(); + }); + + it('click on create visualize wizard', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await a11y.testAppSnapshot(); + }); + + it.skip('create visualize button', async () => { + await PageObjects.visualize.clickNewVisualization(); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/test/accessibility/config.ts b/test/accessibility/config.ts index d73a73820a1172..48fe97e2d0e38a 100644 --- a/test/accessibility/config.ts +++ b/test/accessibility/config.ts @@ -29,7 +29,10 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ require.resolve('./apps/discover'), + require.resolve('./apps/dashboard'), + require.resolve('./apps/visualize'), require.resolve('./apps/management'), + // require.resolve('./apps/console'), require.resolve('./apps/home'), ], pageObjects, diff --git a/test/api_integration/apis/ui_metric/ui_metric.js b/test/api_integration/apis/ui_metric/ui_metric.js index 51959bf5f7fda8..3669d0e21c7763 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.js +++ b/test/api_integration/apis/ui_metric/ui_metric.js @@ -49,10 +49,10 @@ export default function ({ getService }) { } }; await supertest - .post('/api/telemetry/report') + .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') .set('content-type', 'application/json') - .send(report) + .send({ report }) .expect(200); const response = await es.search({ index: '.kibana', q: 'type:ui-metric' }); @@ -77,10 +77,10 @@ export default function ({ getService }) { } }; await supertest - .post('/api/telemetry/report') + .post('/api/ui_metric/report') .set('kbn-xsrf', 'kibana') .set('content-type', 'application/json') - .send(report) + .send({ report }) .expect(200); const response = await es.search({ index: '.kibana', q: 'type:ui-metric' }); diff --git a/test/common/services/security/user.ts b/test/common/services/security/user.ts index e1c9b3fb998add..ae02127043234c 100644 --- a/test/common/services/security/user.ts +++ b/test/common/services/security/user.ts @@ -38,7 +38,7 @@ export class User { public async create(username: string, user: any) { this.log.debug(`creating user ${username}`); const { data, status, statusText } = await this.axios.post( - `/api/security/v1/users/${username}`, + `/internal/security/users/${username}`, { username, ...user, @@ -55,7 +55,7 @@ export class User { public async delete(username: string) { this.log.debug(`deleting user ${username}`); const { data, status, statusText } = await this.axios.delete( - `/api/security/v1/users/${username}` + `/internal/security/users/${username}` ); if (status !== 204) { throw new Error( diff --git a/test/examples/README.md b/test/examples/README.md new file mode 100644 index 00000000000000..44656f949bc72a --- /dev/null +++ b/test/examples/README.md @@ -0,0 +1,23 @@ +# Example plugin functional tests + +This folder contains functional tests for the example plugins. + +## Run the test + +To run these tests during development you can use the following commands: + +``` +# Start the test server (can continue running) +node scripts/functional_tests_server.js --config test/examples/config.js +# Start a test run +node scripts/functional_test_runner.js --config test/examples/config.js +``` + +## Run Kibana with a test plugin + +In case you want to start Kibana with the example plugins, you can just run: + +``` +yarn start --run-examples +``` + diff --git a/test/examples/config.js b/test/examples/config.js new file mode 100644 index 00000000000000..b954390dc54ad1 --- /dev/null +++ b/test/examples/config.js @@ -0,0 +1,55 @@ +/* + * 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 path from 'path'; +import { services } from '../plugin_functional/services'; + +export default async function ({ readConfigFile }) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + testFiles: [ + require.resolve('./search'), + ], + services: { + ...functionalConfig.get('services'), + ...services, + }, + pageObjects: functionalConfig.get('pageObjects'), + servers: functionalConfig.get('servers'), + esTestCluster: functionalConfig.get('esTestCluster'), + apps: functionalConfig.get('apps'), + esArchiver: { + directory: path.resolve(__dirname, '../es_archives') + }, + screenshots: functionalConfig.get('screenshots'), + junit: { + reportName: 'Example plugin functional tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig.get('kbnTestServer.serverArgs'), + '--run-examples', + // Required to run examples + '--env.name=development', + ], + }, + }; +} diff --git a/test/plugin_functional/test_suites/search/demo_data.ts b/test/examples/search/demo_data.ts similarity index 100% rename from test/plugin_functional/test_suites/search/demo_data.ts rename to test/examples/search/demo_data.ts diff --git a/test/plugin_functional/test_suites/search/es_search.ts b/test/examples/search/es_search.ts similarity index 100% rename from test/plugin_functional/test_suites/search/es_search.ts rename to test/examples/search/es_search.ts diff --git a/test/plugin_functional/test_suites/search/index.ts b/test/examples/search/index.ts similarity index 100% rename from test/plugin_functional/test_suites/search/index.ts rename to test/examples/search/index.ts diff --git a/test/functional/apps/dashboard/dashboard_save.js b/test/functional/apps/dashboard/dashboard_save.js index 8e4c4d66b3844b..88a1c592ec74ed 100644 --- a/test/functional/apps/dashboard/dashboard_save.js +++ b/test/functional/apps/dashboard/dashboard_save.js @@ -55,6 +55,7 @@ export default function ({ getPageObjects }) { await PageObjects.dashboard.clickNewDashboard(); await PageObjects.dashboard.enterDashboardTitleAndClickSave(dashboardName, { waitDialogIsClosed: false }); + await PageObjects.dashboard.ensureDuplicateTitleCallout(); await PageObjects.dashboard.clickSave(); // This is important since saving a new dashboard will cause a refresh of the page. We have to diff --git a/test/functional/apps/dashboard/empty_dashboard.js b/test/functional/apps/dashboard/empty_dashboard.js index 1614c8ebf49a96..f50a0ece293a95 100644 --- a/test/functional/apps/dashboard/empty_dashboard.js +++ b/test/functional/apps/dashboard/empty_dashboard.js @@ -24,9 +24,12 @@ export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const dashboardAddPanel = getService('dashboardAddPanel'); + const dashboardVisualizations = getService('dashboardVisualizations'); + const dashboardExpect = getService('dashboardExpect'); const PageObjects = getPageObjects(['common', 'dashboard']); - describe('empty dashboard', () => { + // FLAKY: https://github.com/elastic/kibana/issues/48236 + describe.skip('empty dashboard', () => { before(async () => { await esArchiver.load('dashboard/current/kibana'); await kibanaServer.uiSettings.replace({ @@ -47,13 +50,18 @@ export default function ({ getService, getPageObjects }) { expect(addButtonExists).to.be(true); }); - // Flaky test: https://github.com/elastic/kibana/issues/48236 it.skip('should open add panel when add button is clicked', async () => { await testSubjects.click('emptyDashboardAddPanelButton'); const isAddPanelOpen = await dashboardAddPanel.isAddPanelOpen(); expect(isAddPanelOpen).to.be(true); }); + it('should add new visualization from dashboard', async () => { + await testSubjects.click('addVisualizationButton'); + await dashboardVisualizations.createAndAddMarkdown({ name: 'Dashboard Test Markdown', markdown: 'Markdown text' }, false); + await PageObjects.dashboard.waitForRenderComplete(); + await dashboardExpect.markdownWithValuesExists(['Markdown text']); + }); }); } diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index 73424a3df55dae..f7b0cf41d30e43 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -376,6 +376,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } } + async ensureDuplicateTitleCallout() { + await testSubjects.existOrFail('titleDupicateWarnMsg'); + } + /** * @param dashboardTitle {String} */ diff --git a/test/functional/services/dashboard/visualizations.js b/test/functional/services/dashboard/visualizations.js index 8cde98861ca885..97433a1e4923cc 100644 --- a/test/functional/services/dashboard/visualizations.js +++ b/test/functional/services/dashboard/visualizations.js @@ -73,14 +73,16 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }) { await dashboardAddPanel.addSavedSearch(name); } - async createAndAddMarkdown({ name, markdown }) { + async createAndAddMarkdown({ name, markdown }, checkForAddPanel = true) { log.debug(`createAndAddMarkdown(${markdown})`); const inViewMode = await PageObjects.dashboard.getIsInViewMode(); if (inViewMode) { await PageObjects.dashboard.switchToEditMode(); } - await dashboardAddPanel.ensureAddPanelIsShowing(); - await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); + if (checkForAddPanel) { + await dashboardAddPanel.ensureAddPanelIsShowing(); + await dashboardAddPanel.clickAddNewEmbeddableLink('visualization'); + } await PageObjects.visualize.clickMarkdownWidget(); await PageObjects.visualize.setMarkdownTxt(markdown); await PageObjects.visualize.clickGo(); diff --git a/test/functional/services/screenshots.ts b/test/functional/services/screenshots.ts index ddafa211ece7fe..9e673fe919a74c 100644 --- a/test/functional/services/screenshots.ts +++ b/test/functional/services/screenshots.ts @@ -22,6 +22,7 @@ import { writeFile, readFileSync, mkdir } from 'fs'; import { promisify } from 'util'; import del from 'del'; + import { comparePngs } from './lib/compare_pngs'; import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; @@ -32,6 +33,7 @@ const writeFileAsync = promisify(writeFile); export async function ScreenshotsProvider({ getService }: FtrProviderContext) { const log = getService('log'); const config = getService('config'); + const failureMetadata = getService('failureMetadata'); const browser = getService('browser'); const SESSION_DIRECTORY = resolve(config.get('screenshots.directory'), 'session'); @@ -68,11 +70,15 @@ export async function ScreenshotsProvider({ getService }: FtrProviderContext) { } async take(name: string, el?: WebElementWrapper) { - return await this._take(resolve(SESSION_DIRECTORY, `${name}.png`), el); + const path = resolve(SESSION_DIRECTORY, `${name}.png`); + await this._take(path, el); + failureMetadata.addScreenshot(name, path); } async takeForFailure(name: string, el?: WebElementWrapper) { - await this._take(resolve(FAILURE_DIRECTORY, `${name}.png`), el); + const path = resolve(FAILURE_DIRECTORY, `${name}.png`); + await this._take(path, el); + failureMetadata.addScreenshot(`failure[${name}]`, path); } async _take(path: string, el?: WebElementWrapper) { diff --git a/test/plugin_functional/config.js b/test/plugin_functional/config.js index a6316c607a7c79..4ca3d86f7043cf 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -33,19 +33,7 @@ export default async function ({ readConfigFile }) { require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/panel_actions'), - require.resolve('./test_suites/search'), - - /** - * @todo Work on re-enabling this test suite after this is merged. These tests pass - * locally but on CI they fail. The error on CI says "TypeError: Cannot read - * property 'overlays' of null". Possibly those are `overlays` from - * `npStart.core.overlays`, possibly `npStart.core` is `null` on CI, but - * available when this test suite is executed locally. - * - * See issue: https://github.com/elastic/kibana/issues/43087 - */ - // require.resolve('./test_suites/embeddable_explorer'), - + require.resolve('./test_suites/embeddable_explorer'), require.resolve('./test_suites/core_plugins'), ], services: { diff --git a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx index 56cc1cb4ab425d..5c8e1d03d5a4ac 100644 --- a/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx +++ b/test/plugin_functional/plugins/core_plugin_b/public/plugin.tsx @@ -24,6 +24,7 @@ declare global { interface Window { corePluginB?: string; hasAccessToInjectedMetadata?: boolean; + receivedStartServices?: boolean; env?: PluginInitializerContext['env']; } } @@ -40,6 +41,9 @@ export class CorePluginBPlugin public setup(core: CoreSetup, deps: CorePluginBDeps) { window.corePluginB = `Plugin A said: ${deps.core_plugin_a.getGreeting()}`; window.hasAccessToInjectedMetadata = 'getInjectedVar' in core.injectedMetadata; + core.getStartServices().then(([coreStart, plugins]) => { + window.receivedStartServices = 'overlays' in coreStart; + }); core.application.register({ id: 'bar', diff --git a/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts b/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts index 6988ed82f34a7e..51b5d2aaf35876 100644 --- a/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts +++ b/test/plugin_functional/plugins/core_plugin_legacy/public/index.ts @@ -22,8 +22,8 @@ import { npSetup } from 'ui/new_platform'; npSetup.core.application.register({ id: 'core_legacy_compat', title: 'Core Legacy Compat', - async mount(...args) { + async mount(context, params) { const { renderApp } = await import('./application'); - return renderApp(...args); + return renderApp(context, params); }, }); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts index 1928d7ac72313b..57b5ad086e6a78 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/legacy.ts @@ -18,6 +18,8 @@ */ /* eslint-disable @kbn/eslint/no-restricted-paths */ import 'ui/autoload/all'; + +import 'uiExports/interpreter'; import 'uiExports/embeddableFactories'; import 'uiExports/embeddableActions'; diff --git a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts index a971921ad3ed83..ff535835464870 100644 --- a/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts +++ b/test/plugin_functional/test_suites/core_plugins/ui_plugins.ts @@ -36,28 +36,41 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider expect(corePluginB).to.equal(`Plugin A said: Hello from Plugin A!`); }); }); + describe('have injectedMetadata service provided', function describeIndexTests() { before(async () => { await PageObjects.common.navigateToApp('bar'); }); - it('should attach string to window.corePluginB', async () => { + it('should attach boolean to window.hasAccessToInjectedMetadata', async () => { const hasAccessToInjectedMetadata = await browser.execute( 'return window.hasAccessToInjectedMetadata' ); expect(hasAccessToInjectedMetadata).to.equal(true); }); }); + describe('have env data provided', function describeIndexTests() { before(async () => { await PageObjects.common.navigateToApp('bar'); }); - it('should attach pluginContext to window.corePluginB', async () => { + it('should attach pluginContext to window.env', async () => { const envData: any = await browser.execute('return window.env'); expect(envData.mode.dev).to.be(true); expect(envData.packageInfo.version).to.be.a('string'); }); }); + + describe('have access to start services via coreSetup.getStartServices', function describeIndexTests() { + before(async () => { + await PageObjects.common.navigateToApp('bar'); + }); + + it('should attach boolean to window.receivedStartServices', async () => { + const receivedStartServices = await browser.execute('return window.receivedStartServices'); + expect(receivedStartServices).to.equal(true); + }); + }); }); } diff --git a/vars/agentInfo.groovy b/vars/agentInfo.groovy new file mode 100644 index 00000000000000..b53ed23f81ff0c --- /dev/null +++ b/vars/agentInfo.groovy @@ -0,0 +1,42 @@ +def print() { + try { + def startTime = sh(script: "date -d '-3 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() + def endTime = sh(script: "date -d '+1 hour 30 minutes' -Iseconds | sed s/+/%2B/", returnStdout: true).trim() + + def resourcesUrl = + ( + "https://infra-stats.elastic.co/app/kibana#/visualize/edit/8bd92360-1b92-11ea-b719-aba04518cc34" + + "?_g=(time:(from:'${startTime}',to:'${endTime}'))" + + "&_a=(query:'host.name:${env.NODE_NAME}')" + ) + .replaceAll("'", '%27') // Need to escape ' because of the shell echo below, but can't really replace "'" with "\'" because of groovy sandbox + .replaceAll(/\)$/, '%29') // This is just here because the URL parsing in the Jenkins console doesn't work right + + def logsStartTime = sh(script: "date -d '-3 minutes' +%s", returnStdout: true).trim() + def logsUrl = + ( + "https://infra-stats.elastic.co/app/infra#/logs" + + "?_g=()&flyoutOptions=(flyoutId:!n,flyoutVisibility:hidden,surroundingLogsId:!n)" + + "&logFilter=(expression:'host.name:${env.NODE_NAME}',kind:kuery)" + + "&logPosition=(position:(time:${logsStartTime}000),streamLive:!f)" + ) + .replaceAll("'", '%27') + .replaceAll('\\)', '%29') + + sh script: """ + set +x + echo 'Resource Graph:' + echo '${resourcesUrl}' + echo '' + echo 'Agent Logs:' + echo '${logsUrl}' + echo '' + echo 'SSH Command:' + echo "ssh -F ssh_config \$(hostname --ip-address)" + """, label: "Worker/Agent/Node debug links" + } catch(ex) { + print ex.toString() + } +} + +return this diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 77907a07addd15..18f214554b444d 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,47 +1,45 @@ -def withWorkers(name, preWorkerClosure = {}, workerClosures = [:]) { +def withWorkers(machineName, preWorkerClosure = {}, workerClosures = [:]) { return { jobRunner('tests-xl', true) { - try { - doSetup() - preWorkerClosure() - - def nextWorker = 1 - def worker = { workerClosure -> - def workerNumber = nextWorker - nextWorker++ - - return { - // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time - def delay = (workerNumber-1)*20 - sleep(delay) - - workerClosure(workerNumber) + withGcsArtifactUpload(machineName, { + try { + doSetup() + preWorkerClosure() + + def nextWorker = 1 + def worker = { workerClosure -> + def workerNumber = nextWorker + nextWorker++ + + return { + // This delay helps smooth out CPU load caused by ES/Kibana instances starting up at the same time + def delay = (workerNumber-1)*20 + sleep(delay) + + workerClosure(workerNumber) + } } - } - - def workers = [:] - workerClosures.each { workerName, workerClosure -> - workers[workerName] = worker(workerClosure) - } - parallel(workers) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } + def workers = [:] + workerClosures.each { workerName, workerClosure -> + workers[workerName] = worker(workerClosure) + } - catchError { - runErrorReporter() - } + parallel(workers) + } finally { + catchError { + runErrorReporter() + } - catchError { - runbld.junit() - } + catchError { + runbld.junit() + } - catchError { - publishJunit() + catchError { + publishJunit() + } } - } + }) } } } @@ -96,19 +94,19 @@ def legacyJobRunner(name) { "JOB=${name}", ]) { jobRunner('linux && immutable', false) { - try { - runbld('.ci/run.sh', "Execute ${name}", true) - } finally { - catchError { - uploadAllGcsArtifacts(name) - } - catchError { - runErrorReporter() - } - catchError { - publishJunit() + withGcsArtifactUpload(name, { + try { + runbld('.ci/run.sh', "Execute ${name}", true) + } finally { + catchError { + runErrorReporter() + } + + catchError { + publishJunit() + } } - } + }) } } } @@ -118,6 +116,8 @@ def legacyJobRunner(name) { def jobRunner(label, useRamDisk, closure) { node(label) { + agentInfo.print() + if (useRamDisk) { // Move to a temporary workspace, so that we can symlink the real workspace into /dev/shm def originalWorkspace = env.WORKSPACE @@ -171,19 +171,18 @@ def jobRunner(label, useRamDisk, closure) { // TODO what should happen if GCS, Junit, or email publishing fails? Unstable build? Failed build? -def uploadGcsArtifact(workerName, pattern) { - def storageLocation = "gs://kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" // TODO - +def uploadGcsArtifact(uploadPrefix, pattern) { googleStorageUpload( credentialsId: 'kibana-ci-gcs-plugin', - bucket: storageLocation, + bucket: "gs://${uploadPrefix}", pattern: pattern, sharedPublicly: true, showInline: true, ) } -def uploadAllGcsArtifacts(workerName) { +def withGcsArtifactUpload(workerName, closure) { + def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', 'target/junit/**/*', @@ -194,9 +193,19 @@ def uploadAllGcsArtifacts(workerName) { 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', ] - ARTIFACT_PATTERNS.each { pattern -> - uploadGcsArtifact(workerName, pattern) - } + withEnv([ + "GCS_UPLOAD_PREFIX=${uploadPrefix}" + ], { + try { + closure() + } finally { + catchError { + ARTIFACT_PATTERNS.each { pattern -> + uploadGcsArtifact(uploadPrefix, pattern) + } + } + } + }) } def publishJunit() { diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 180aafe504c634..7e86d2f1dc4357 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -12,7 +12,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "legacy/plugins/file_upload", - "xpack.graph": "legacy/plugins/graph", + "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], "xpack.grokDebugger": "legacy/plugins/grokdebugger", "xpack.idxMgmt": "legacy/plugins/index_management", "xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 199232262773d3..f8d07668d0aae1 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -20,7 +20,8 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, - '^plugins/watcher/models/(.*)': `${xPackKibanaDirectory}/legacy/plugins/watcher/public/models/$1`, + '^plugins/watcher/np_ready/application/models/(.*)': + `${xPackKibanaDirectory}/legacy/plugins/watcher/public/np_ready/application/models/$1`, '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, diff --git a/x-pack/legacy/common/poller.d.ts b/x-pack/legacy/common/poller.d.ts index c23d18dd62e872..df39d93a28a815 100644 --- a/x-pack/legacy/common/poller.d.ts +++ b/x-pack/legacy/common/poller.d.ts @@ -8,4 +8,7 @@ export declare class Poller { constructor(options: any); public start(): void; + public stop(): void; + public isRunning(): boolean; + public getPollFrequency(): number; } diff --git a/x-pack/legacy/plugins/actions/server/plugin.ts b/x-pack/legacy/plugins/actions/server/plugin.ts index 27eead7d736c16..510e2a3b948946 100644 --- a/x-pack/legacy/plugins/actions/server/plugin.ts +++ b/x-pack/legacy/plugins/actions/server/plugin.ts @@ -69,30 +69,6 @@ export class Plugin { this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); this.defaultKibanaIndex = (await this.kibana$.pipe(first()).toPromise()).index; - plugins.xpack_main.registerFeature({ - id: 'actions', - name: 'Actions', - app: ['actions', 'kibana'], - privileges: { - all: { - savedObject: { - all: ['action', 'action_task_params'], - read: [], - }, - ui: [], - api: ['actions-read', 'actions-all'], - }, - read: { - savedObject: { - all: ['action_task_params'], - read: ['action'], - }, - ui: [], - api: ['actions-read'], - }, - }, - }); - // Encrypted attributes // - `secrets` properties will be encrypted // - `config` will be included in AAD diff --git a/x-pack/legacy/plugins/actions/server/shim.ts b/x-pack/legacy/plugins/actions/server/shim.ts index 1af62d276f10bb..c40e4ea79d1c39 100644 --- a/x-pack/legacy/plugins/actions/server/shim.ts +++ b/x-pack/legacy/plugins/actions/server/shim.ts @@ -9,7 +9,7 @@ import { Legacy } from 'kibana'; import * as Rx from 'rxjs'; import { ActionsConfigType } from './types'; import { TaskManager } from '../../task_manager'; -import { XPackMainPlugin } from '../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces'; import { diff --git a/x-pack/legacy/plugins/alerting/server/plugin.ts b/x-pack/legacy/plugins/alerting/server/plugin.ts index c50bc795757f3e..32e57536873670 100644 --- a/x-pack/legacy/plugins/alerting/server/plugin.ts +++ b/x-pack/legacy/plugins/alerting/server/plugin.ts @@ -60,30 +60,6 @@ export class Plugin { ): Promise { this.adminClient = await core.elasticsearch.adminClient$.pipe(first()).toPromise(); - plugins.xpack_main.registerFeature({ - id: 'alerting', - name: 'Alerting', - app: ['alerting', 'kibana'], - privileges: { - all: { - savedObject: { - all: ['alert'], - read: [], - }, - ui: [], - api: ['alerting-read', 'alerting-all'], - }, - read: { - savedObject: { - all: [], - read: ['alert'], - }, - ui: [], - api: ['alerting-read'], - }, - }, - }); - // Encrypted attributes plugins.encryptedSavedObjects.registerType({ type: 'alert', diff --git a/x-pack/legacy/plugins/alerting/server/shim.ts b/x-pack/legacy/plugins/alerting/server/shim.ts index 0ee1ef843d7d05..ef1f1b41049e5e 100644 --- a/x-pack/legacy/plugins/alerting/server/shim.ts +++ b/x-pack/legacy/plugins/alerting/server/shim.ts @@ -8,7 +8,7 @@ import Hapi from 'hapi'; import { Legacy } from 'kibana'; import { LegacySpacesPlugin as SpacesPluginStartContract } from '../../spaces'; import { TaskManager } from '../../task_manager'; -import { XPackMainPlugin } from '../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; import KbnServer from '../../../../../src/legacy/server/kbn_server'; import { PluginSetupContract as EncryptedSavedObjectsSetupContract, diff --git a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml index 900fb8d47cecff..3082391f23a15d 100644 --- a/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml +++ b/x-pack/legacy/plugins/apm/cypress/ci/kibana.dev.yml @@ -1,57 +1,4 @@ ## # Disabled plugins ######################## -# data.enabled: false -# interpreter.enabled: false -# visualizations.enabled: false -# xpack.apm.enabled: false -# console.enabled: false -console_extensions.enabled: false -dashboard_embeddable_container.enabled: false -dashboard_mode.enabled: false -embeddable_api.enabled: false -file_upload.enabled: false -# input_control_vis.enabled: false -inspector_views.enabled: false -kibana_react.enabled: false -markdown_vis.enabled: false -metric_vis.enabled: false -metrics.enabled: false -region_map.enabled: false -table_vis.enabled: false -tagcloud.enabled: false -tile_map.enabled: false -timelion.enabled: false -ui_metric.enabled: false -vega.enabled: false -xpack.actions.enabled: false -xpack.alerting.enabled: false -xpack.beats.enabled: false -xpack.canvas.enabled: false -xpack.cloud.enabled: false -xpack.code.enabled: false -xpack.encryptedSavedObjects.enabled: false -xpack.graph.enabled: false -xpack.grokdebugger.enabled: false -xpack.index_management.enabled: false -xpack.infra.enabled: false -# xpack.license_management.enabled: false -xpack.lens.enabled: false -xpack.logstash.enabled: false -xpack.maps.enabled: false -xpack.ml.enabled: false -xpack.monitoring.enabled: false -xpack.oss_telemetry.enabled: false -xpack.remote_clusters.enabled: false -xpack.rollup.enabled: false -xpack.searchprofiler.enabled: false -# xpack.security.enabled: false -xpack.siem.enabled: false -xpack.snapshot_restore.enabled: false -xpack.spaces.enabled: false -xpack.task_manager.enabled: false -xpack.tilemap.enabled: false -xpack.upgrade_assistant.enabled: false -xpack.uptime.enabled: false -xpack.watcher.enabled: false logging.verbose: true diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index 91745246687d99..cf2cbd25072151 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import { resolve } from 'path'; -import { APMPluginContract } from '../../../plugins/apm/server/plugin'; +import { APMPluginContract } from '../../../plugins/apm/server'; import { LegacyPluginInitializer } from '../../../../src/legacy/types'; import mappings from './mappings.json'; import { makeApmUsageCollector } from './server/lib/apm_telemetry'; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx index 272e2561b7fd7d..711290942cea16 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/Home.test.tsx @@ -7,15 +7,15 @@ import { shallow } from 'enzyme'; import React from 'react'; import { Home } from '../Home'; -import { MockPluginContextWrapper } from '../../../utils/testHelpers'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; describe('Home component', () => { it('should render services', () => { expect( shallow( - + - + ) ).toMatchSnapshot(); }); @@ -23,9 +23,9 @@ describe('Home component', () => { it('should render traces', () => { expect( shallow( - + - + ) ).toMatchSnapshot(); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 664a71c934a4e5..7809734dbf2adb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -4,10 +4,32 @@ exports[`Home component should render services 1`] = ` @@ -21,10 +43,32 @@ exports[`Home component should render traces 1`] = ` diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx index 2b3f1368f6fa0a..4c98618d7de8a4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/index.tsx @@ -25,7 +25,7 @@ import { EuiTabLink } from '../../shared/EuiTabLink'; import { SettingsLink } from '../../shared/Links/apm/SettingsLink'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; import { ServiceMap } from '../ServiceMap'; -import { usePlugins } from '../../../new-platform/plugin'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; function getHomeTabs({ serviceMapEnabled = false @@ -82,9 +82,8 @@ interface Props { } export function Home({ tab }: Props) { - const { apm } = usePlugins(); - const { serviceMapEnabled } = apm.config; - const homeTabs = getHomeTabs({ serviceMapEnabled }); + const { config } = useApmPluginContext(); + const homeTabs = getHomeTabs(config); const selectedTab = homeTabs.find( homeTab => homeTab.name === tab ) as $ElementType; diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.tsx new file mode 100644 index 00000000000000..5bf8cb8271fa46 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.test.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 { mount } from 'enzyme'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; +import { + mockApmPluginContextValue, + MockApmPluginContextWrapper +} from '../../../utils/testHelpers'; +import { routes } from './route_config'; +import { UpdateBreadcrumbs } from './UpdateBreadcrumbs'; + +const setBreadcrumbs = jest.fn(); + +function expectBreadcrumbToMatchSnapshot(route: string, params = '') { + mount( + + + + + + ); + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs.mock.calls[0][0]).toMatchSnapshot(); +} + +describe('UpdateBreadcrumbs', () => { + let realDoc: Document; + + beforeEach(() => { + realDoc = window.document; + (window.document as any) = { + title: 'Kibana' + }; + setBreadcrumbs.mockReset(); + }); + + afterEach(() => { + (window.document as any) = realDoc; + }); + + it('Homepage', () => { + expectBreadcrumbToMatchSnapshot('/'); + expect(window.document.title).toMatchInlineSnapshot(`"APM"`); + }); + + it('/services/:serviceName/errors/:groupId', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors/myGroupId'); + expect(window.document.title).toMatchInlineSnapshot( + `"myGroupId | Errors | opbeans-node | Services | APM"` + ); + }); + + it('/services/:serviceName/errors', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors'); + expect(window.document.title).toMatchInlineSnapshot( + `"Errors | opbeans-node | Services | APM"` + ); + }); + + it('/services/:serviceName/transactions', () => { + expectBreadcrumbToMatchSnapshot('/services/opbeans-node/transactions'); + expect(window.document.title).toMatchInlineSnapshot( + `"Transactions | opbeans-node | Services | APM"` + ); + }); + + it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { + expectBreadcrumbToMatchSnapshot( + '/services/opbeans-node/transactions/view', + 'transactionName=my-transaction-name' + ); + expect(window.document.title).toMatchInlineSnapshot( + `"my-transaction-name | Transactions | opbeans-node | Services | APM"` + ); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx index 6568c9151bfd92..8960af0f21fd29 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/UpdateBreadcrumbs.tsx @@ -6,19 +6,19 @@ import { Location } from 'history'; import React from 'react'; -import { LegacyCoreStart } from 'src/core/public'; -import { useKibanaCore } from '../../../../../observability/public'; +import { AppMountContext } from 'src/core/public'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; import { Breadcrumb, ProvideBreadcrumbs, BreadcrumbRoute } from './ProvideBreadcrumbs'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; interface Props { location: Location; breadcrumbs: Breadcrumb[]; - core: LegacyCoreStart; + core: AppMountContext['core']; } function getTitleFromBreadCrumbs(breadcrumbs: Breadcrumb[]) { @@ -57,7 +57,8 @@ interface UpdateBreadcrumbsProps { } export function UpdateBreadcrumbs({ routes }: UpdateBreadcrumbsProps) { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); + return ( - - - ); - expect(coreMock.chrome.setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(coreMock.chrome.setBreadcrumbs.mock.calls[0][0]).toMatchSnapshot(); -} - -describe('UpdateBreadcrumbs', () => { - let realDoc; - - beforeEach(() => { - realDoc = global.document; - global.document = { - title: 'Kibana' - }; - coreMock.chrome.setBreadcrumbs.mockReset(); - }); - - afterEach(() => { - global.document = realDoc; - }); - - it('Homepage', () => { - expectBreadcrumbToMatchSnapshot('/'); - expect(global.document.title).toMatchInlineSnapshot(`"APM"`); - }); - - it('/services/:serviceName/errors/:groupId', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors/myGroupId'); - expect(global.document.title).toMatchInlineSnapshot( - `"myGroupId | Errors | opbeans-node | Services | APM"` - ); - }); - - it('/services/:serviceName/errors', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/errors'); - expect(global.document.title).toMatchInlineSnapshot( - `"Errors | opbeans-node | Services | APM"` - ); - }); - - it('/services/:serviceName/transactions', () => { - expectBreadcrumbToMatchSnapshot('/services/opbeans-node/transactions'); - expect(global.document.title).toMatchInlineSnapshot( - `"Transactions | opbeans-node | Services | APM"` - ); - }); - - it('/services/:serviceName/transactions/view?transactionName=my-transaction-name', () => { - expectBreadcrumbToMatchSnapshot( - '/services/opbeans-node/transactions/view', - 'transactionName=my-transaction-name' - ); - expect(global.document.title).toMatchInlineSnapshot( - `"my-transaction-name | Transactions | opbeans-node | Services | APM"` - ); - }); -}); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx index 1d76a985736171..2f7df3c5a4acd7 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Main/route_config/index.tsx @@ -42,187 +42,175 @@ const renderAsRedirectTo = (to: string) => { ); }; -export function getRoutes({ - serviceMapEnabled -}: { - serviceMapEnabled: boolean; -}): BreadcrumbRoute[] { - const routes: BreadcrumbRoute[] = [ - { - exact: true, - path: '/', - render: renderAsRedirectTo('/services'), - breadcrumb: 'APM', - name: RouteName.HOME - }, - { - exact: true, - path: '/services', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { - defaultMessage: 'Services' - }), - name: RouteName.SERVICES - }, - { - exact: true, - path: '/traces', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { - defaultMessage: 'Traces' - }), - name: RouteName.TRACES - }, - { - exact: true, - path: '/settings', - render: renderAsRedirectTo('/settings/agent-configuration'), - breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { - defaultMessage: 'Settings' - }), - name: RouteName.SETTINGS - }, - { - exact: true, - path: '/settings/apm-indices', - component: () => ( - - - - ), - breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', { - defaultMessage: 'Indices' - }), - name: RouteName.INDICES - }, - { - exact: true, - path: '/settings/agent-configuration', - component: () => ( - - - - ), - breadcrumb: i18n.translate( - 'xpack.apm.breadcrumb.settings.agentConfigurationTitle', - { - defaultMessage: 'Agent Configuration' - } - ), - name: RouteName.AGENT_CONFIGURATION - }, - { - exact: true, - path: '/services/:serviceName', - breadcrumb: ({ match }) => match.params.serviceName, - render: (props: RouteComponentProps) => - renderAsRedirectTo( - `/services/${props.match.params.serviceName}/transactions` - )(props), - name: RouteName.SERVICE - }, - // errors - { - exact: true, - path: '/services/:serviceName/errors/:groupId', - component: ErrorGroupDetails, - breadcrumb: ({ match }) => match.params.groupId, - name: RouteName.ERROR - }, - { - exact: true, - path: '/services/:serviceName/errors', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { - defaultMessage: 'Errors' - }), - name: RouteName.ERRORS - }, - // transactions - { - exact: true, - path: '/services/:serviceName/transactions', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { - defaultMessage: 'Transactions' - }), - name: RouteName.TRANSACTIONS - }, - // metrics - { - exact: true, - path: '/services/:serviceName/metrics', - component: () => , - breadcrumb: metricsBreadcrumb, - name: RouteName.METRICS - }, - // service nodes, only enabled for java agents for now - { - exact: true, - path: '/services/:serviceName/nodes', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', { - defaultMessage: 'JVMs' - }), - name: RouteName.SERVICE_NODES - }, - // node metrics - { - exact: true, - path: '/services/:serviceName/nodes/:serviceNodeName/metrics', - component: () => , - breadcrumb: ({ location }) => { - const { serviceNodeName } = resolveUrlParams(location, {}); +export const routes: BreadcrumbRoute[] = [ + { + exact: true, + path: '/', + render: renderAsRedirectTo('/services'), + breadcrumb: 'APM', + name: RouteName.HOME + }, + { + exact: true, + path: '/services', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.servicesTitle', { + defaultMessage: 'Services' + }), + name: RouteName.SERVICES + }, + { + exact: true, + path: '/traces', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.tracesTitle', { + defaultMessage: 'Traces' + }), + name: RouteName.TRACES + }, + { + exact: true, + path: '/settings', + render: renderAsRedirectTo('/settings/agent-configuration'), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { + defaultMessage: 'Settings' + }), + name: RouteName.SETTINGS + }, + { + exact: true, + path: '/settings/apm-indices', + component: () => ( + + + + ), + breadcrumb: i18n.translate('xpack.apm.breadcrumb.settings.indicesTitle', { + defaultMessage: 'Indices' + }), + name: RouteName.INDICES + }, + { + exact: true, + path: '/settings/agent-configuration', + component: () => ( + + + + ), + breadcrumb: i18n.translate( + 'xpack.apm.breadcrumb.settings.agentConfigurationTitle', + { + defaultMessage: 'Agent Configuration' + } + ), + name: RouteName.AGENT_CONFIGURATION + }, + { + exact: true, + path: '/services/:serviceName', + breadcrumb: ({ match }) => match.params.serviceName, + render: (props: RouteComponentProps) => + renderAsRedirectTo( + `/services/${props.match.params.serviceName}/transactions` + )(props), + name: RouteName.SERVICE + }, + // errors + { + exact: true, + path: '/services/:serviceName/errors/:groupId', + component: ErrorGroupDetails, + breadcrumb: ({ match }) => match.params.groupId, + name: RouteName.ERROR + }, + { + exact: true, + path: '/services/:serviceName/errors', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.errorsTitle', { + defaultMessage: 'Errors' + }), + name: RouteName.ERRORS + }, + // transactions + { + exact: true, + path: '/services/:serviceName/transactions', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.transactionsTitle', { + defaultMessage: 'Transactions' + }), + name: RouteName.TRANSACTIONS + }, + // metrics + { + exact: true, + path: '/services/:serviceName/metrics', + component: () => , + breadcrumb: metricsBreadcrumb, + name: RouteName.METRICS + }, + // service nodes, only enabled for java agents for now + { + exact: true, + path: '/services/:serviceName/nodes', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.nodesTitle', { + defaultMessage: 'JVMs' + }), + name: RouteName.SERVICE_NODES + }, + // node metrics + { + exact: true, + path: '/services/:serviceName/nodes/:serviceNodeName/metrics', + component: () => , + breadcrumb: ({ location }) => { + const { serviceNodeName } = resolveUrlParams(location, {}); - if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { - return UNIDENTIFIED_SERVICE_NODES_LABEL; - } + if (serviceNodeName === SERVICE_NODE_NAME_MISSING) { + return UNIDENTIFIED_SERVICE_NODES_LABEL; + } - return serviceNodeName || ''; - }, - name: RouteName.SERVICE_NODE_METRICS + return serviceNodeName || ''; }, - { - exact: true, - path: '/services/:serviceName/transactions/view', - component: TransactionDetails, - breadcrumb: ({ location }) => { - const query = toQuery(location.search); - return query.transactionName as string; - }, - name: RouteName.TRANSACTION_NAME + name: RouteName.SERVICE_NODE_METRICS + }, + { + exact: true, + path: '/services/:serviceName/transactions/view', + component: TransactionDetails, + breadcrumb: ({ location }) => { + const query = toQuery(location.search); + return query.transactionName as string; }, - { - exact: true, - path: '/link-to/trace/:traceId', - component: TraceLink, - breadcrumb: null, - name: RouteName.LINK_TO_TRACE - } - ]; + name: RouteName.TRANSACTION_NAME + }, + { + exact: true, + path: '/link-to/trace/:traceId', + component: TraceLink, + breadcrumb: null, + name: RouteName.LINK_TO_TRACE + }, - if (serviceMapEnabled) { - routes.push( - { - exact: true, - path: '/service-map', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map' - }), - name: RouteName.SERVICE_MAP - }, - { - exact: true, - path: '/services/:serviceName/service-map', - component: () => , - breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { - defaultMessage: 'Service Map' - }), - name: RouteName.SINGLE_SERVICE_MAP - } - ); + { + exact: true, + path: '/service-map', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map' + }), + name: RouteName.SERVICE_MAP + }, + { + exact: true, + path: '/services/:serviceName/service-map', + component: () => , + breadcrumb: i18n.translate('xpack.apm.breadcrumb.serviceMapTitle', { + defaultMessage: 'Service Map' + }), + name: RouteName.SINGLE_SERVICE_MAP } - - return routes; -} +]; diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx index dda3c494e39adc..75ea02491967e5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceDetailTabs.tsx @@ -21,7 +21,7 @@ import { ServiceNodeOverview } from '../ServiceNodeOverview'; import { useAgentName } from '../../../hooks/useAgentName'; import { ServiceMap } from '../ServiceMap'; import { ServiceMapLink } from '../../shared/Links/apm/ServiceMapLink'; -import { usePlugins } from '../../../new-platform/plugin'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; interface Props { tab: 'transactions' | 'errors' | 'metrics' | 'nodes' | 'service-map'; @@ -31,8 +31,7 @@ export function ServiceDetailTabs({ tab }: Props) { const { urlParams } = useUrlParams(); const { serviceName } = urlParams; const { agentName } = useAgentName(); - const { apm } = usePlugins(); - const { serviceMapEnabled } = apm.config; + const { serviceMapEnabled } = useApmPluginContext().config; if (!serviceName) { // this never happens, urlParams type is not accurate enough diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx index 69f0cf61af2427..a1462c7637358d 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/index.tsx @@ -11,7 +11,7 @@ import { startMLJob } from '../../../../../services/rest/ml'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; import { MachineLearningFlyoutView } from './view'; -import { KibanaCoreContext } from '../../../../../../../observability/public'; +import { ApmPluginContext } from '../../../../../context/ApmPluginContext'; interface Props { isOpen: boolean; @@ -24,7 +24,7 @@ interface State { } export class MachineLearningFlyout extends Component { - static contextType = KibanaCoreContext; + static contextType = ApmPluginContext; public state: State = { isCreatingJob: false @@ -37,7 +37,7 @@ export class MachineLearningFlyout extends Component { }) => { this.setState({ isCreatingJob: true }); try { - const { http } = this.context; + const { http } = this.context.core; const { serviceName } = this.props.urlParams; if (!serviceName) { throw new Error('Service name is required to create this ML job'); @@ -91,7 +91,7 @@ export class MachineLearningFlyout extends Component { }: { transactionType: string; }) => { - const core = this.context; + const { core } = this.context; const { urlParams } = this.props; const { serviceName } = urlParams; @@ -119,7 +119,7 @@ export class MachineLearningFlyout extends Component { } } )}{' '} - + { } )} - +

) }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx index c3d1c8ba1f5b17..31fc4db8f1a2f0 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/MachineLearningFlyout/view.tsx @@ -22,7 +22,6 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useEffect } from 'react'; import { isEmpty } from 'lodash'; -import { useKibanaCore } from '../../../../../../../observability/public'; import { FETCH_STATUS, useFetcher } from '../../../../../hooks/useFetcher'; import { getHasMLJob } from '../../../../../services/rest/ml'; import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink'; @@ -30,6 +29,7 @@ import { MLLink } from '../../../../shared/Links/MachineLearningLinks/MLLink'; import { TransactionSelect } from './TransactionSelect'; import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; import { useServiceTransactionTypes } from '../../../../../hooks/useServiceTransactionTypes'; +import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; interface Props { isCreatingJob: boolean; @@ -51,7 +51,7 @@ export function MachineLearningFlyoutView({ string | undefined >(undefined); - const { http } = useKibanaCore(); + const { http } = useApmPluginContext().core; const { data: hasMLJob = false, status } = useFetcher(() => { if (serviceName && selectedTransactionType) { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx index 3625fb430ff297..85254bee12e134 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/WatcherFlyout.tsx @@ -31,12 +31,11 @@ import moment from 'moment-timezone'; import React, { Component } from 'react'; import styled from 'styled-components'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { KibanaCoreContext } from '../../../../../../observability/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { KibanaLink } from '../../../shared/Links/KibanaLink'; import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; -import { PluginsContext } from '../../../../new-platform/plugin'; +import { ApmPluginContext } from '../../../../context/ApmPluginContext'; type ScheduleKey = keyof Schedule; @@ -77,8 +76,8 @@ export class WatcherFlyout extends Component< WatcherFlyoutProps, WatcherFlyoutState > { - static contextType = KibanaCoreContext; - context!: React.ContextType; + static contextType = ApmPluginContext; + context!: React.ContextType; public state: WatcherFlyoutState = { schedule: 'daily', threshold: 10, @@ -156,7 +155,7 @@ export class WatcherFlyout extends Component< indexPatternTitle: string; }) => () => { const { serviceName } = this.props.urlParams; - const core = this.context; + const { core } = this.context; if (!serviceName) { return; @@ -213,7 +212,7 @@ export class WatcherFlyout extends Component< }; public addErrorToast = () => { - const core = this.context; + const { core } = this.context; core.notifications.toasts.addWarning({ title: i18n.translate( @@ -237,7 +236,7 @@ export class WatcherFlyout extends Component< }; public addSuccessToast = (id: string) => { - const core = this.context; + const { core } = this.context; core.notifications.toasts.addSuccess({ title: i18n.translate( @@ -258,7 +257,7 @@ export class WatcherFlyout extends Component< } } )}{' '} - + @@ -269,7 +268,7 @@ export class WatcherFlyout extends Component< } )} - +

) }); @@ -614,11 +613,11 @@ export class WatcherFlyout extends Component< - - {({ apm }) => { + + {({ config }) => { return ( ); }} - + diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx index 8903900a625c1b..4158bb877e4595 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceDetails/ServiceIntegrations/index.tsx @@ -13,11 +13,11 @@ import { import { i18n } from '@kbn/i18n'; import { memoize } from 'lodash'; import React, { Fragment } from 'react'; -import { KibanaCoreContext } from '../../../../../../observability/public'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { LicenseContext } from '../../../../context/LicenseContext'; import { MachineLearningFlyout } from './MachineLearningFlyout'; import { WatcherFlyout } from './WatcherFlyout'; +import { ApmPluginContext } from '../../../../context/ApmPluginContext'; interface Props { urlParams: IUrlParams; @@ -29,8 +29,8 @@ interface State { type FlyoutName = null | 'ML' | 'Watcher'; export class ServiceIntegrations extends React.Component { - static contextType = KibanaCoreContext; - context!: React.ContextType; + static contextType = ApmPluginContext; + context!: React.ContextType; public state: State = { isPopoverOpen: false, activeFlyout: null }; @@ -67,7 +67,7 @@ export class ServiceIntegrations extends React.Component { }; public getWatcherPanelItems = () => { - const core = this.context; + const { core } = this.context; return [ { diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx index 21a39e19657a1d..0ec9e90a316599 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceNodeMetrics/index.test.tsx @@ -7,11 +7,18 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ServiceNodeMetrics } from '.'; +import { MockApmPluginContextWrapper } from '../../../utils/testHelpers'; describe('ServiceNodeMetrics', () => { describe('render', () => { it('renders', () => { - expect(() => shallow()).not.toThrowError(); + expect(() => + shallow( + + + + ) + ).not.toThrowError(); }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index 9f48880090369d..241f272b54a1d9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -4,42 +4,60 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { ReactChild, FunctionComponent } from 'react'; import { render, wait, waitForElement } from '@testing-library/react'; import { ServiceOverview } from '..'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; -import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; +import { + MockApmPluginContextWrapper, + mockApmPluginContextValue +} from '../../../../utils/testHelpers'; +import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; + +jest.mock('ui/new_platform'); + +function wrapper({ children }: { children: ReactChild }) { + return ( + + {children} + + ); +} function renderServiceOverview() { - return render(); + return render(, { wrapper } as { + wrapper: FunctionComponent<{}>; + }); } -const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - }, - get: jest.fn() - }, - notifications: { - toasts: { - addWarning: () => {} - } - } -} as unknown) as LegacyCoreStart & { - http: { get: jest.Mock }; -}; +const addWarning = jest.fn(); +const httpGet = jest.fn(); describe('Service Overview -> View', () => { beforeEach(() => { // @ts-ignore global.sessionStorage = new SessionStorageMock(); - spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock); // mock urlParams spyOn(urlParamsHooks, 'useUrlParams').and.returnValue({ urlParams: { @@ -62,7 +80,7 @@ describe('Service Overview -> View', () => { it('should render services, when list is not empty', async () => { // mock rest requests - coreMock.http.get.mockResolvedValueOnce({ + httpGet.mockResolvedValueOnce({ hasLegacyData: false, hasHistoricalData: true, items: [ @@ -88,14 +106,14 @@ describe('Service Overview -> View', () => { const { container, getByText } = renderServiceOverview(); // wait for requests to be made - await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1)); + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); await waitForElement(() => getByText('My Python Service')); expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot(); }); it('should render getting started message, when list is empty and no historical data is found', async () => { - coreMock.http.get.mockResolvedValueOnce({ + httpGet.mockResolvedValueOnce({ hasLegacyData: false, hasHistoricalData: false, items: [] @@ -104,7 +122,7 @@ describe('Service Overview -> View', () => { const { container, getByText } = renderServiceOverview(); // wait for requests to be made - await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1)); + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); // wait for elements to be rendered await waitForElement(() => @@ -117,7 +135,7 @@ describe('Service Overview -> View', () => { }); it('should render empty message, when list is empty and historical data is found', async () => { - coreMock.http.get.mockResolvedValueOnce({ + httpGet.mockResolvedValueOnce({ hasLegacyData: false, hasHistoricalData: true, items: [] @@ -126,7 +144,7 @@ describe('Service Overview -> View', () => { const { container, getByText } = renderServiceOverview(); // wait for requests to be made - await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1)); + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); await waitForElement(() => getByText('No services found')); expect(container.querySelectorAll('.euiTableRow')).toMatchSnapshot(); @@ -134,13 +152,7 @@ describe('Service Overview -> View', () => { describe('when legacy data is found', () => { it('renders an upgrade migration notification', async () => { - // create spies - const addWarning = jest.spyOn( - coreMock.notifications.toasts, - 'addWarning' - ); - - coreMock.http.get.mockResolvedValueOnce({ + httpGet.mockResolvedValueOnce({ hasLegacyData: true, hasHistoricalData: true, items: [] @@ -149,7 +161,7 @@ describe('Service Overview -> View', () => { renderServiceOverview(); // wait for requests to be made - await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1)); + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); expect(addWarning).toHaveBeenLastCalledWith( expect.objectContaining({ @@ -161,12 +173,7 @@ describe('Service Overview -> View', () => { describe('when legacy data is not found', () => { it('does not render an upgrade migration notification', async () => { - // create spies - const addWarning = jest.spyOn( - coreMock.notifications.toasts, - 'addWarning' - ); - coreMock.http.get.mockResolvedValueOnce({ + httpGet.mockResolvedValueOnce({ hasLegacyData: false, hasHistoricalData: true, items: [] @@ -175,7 +182,7 @@ describe('Service Overview -> View', () => { renderServiceOverview(); // wait for requests to be made - await wait(() => expect(coreMock.http.get).toHaveBeenCalledTimes(1)); + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); expect(addWarning).not.toHaveBeenCalled(); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx index 0702e092a714f5..05ccc691ecdba8 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -15,9 +15,9 @@ import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useTrackPageview } from '../../../../../infra/public'; -import { useKibanaCore } from '../../../../../observability/public'; import { PROJECTION } from '../../../../common/projections/typings'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; const initalData = { items: [], @@ -28,7 +28,7 @@ const initalData = { let hasDisplayedToast = false; export function ServiceOverview() { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const { urlParams: { start, end }, uiFilters diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx index 8f5d4f4f600d3b..b5a59aea3286d2 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { useCallApmApi } from '../../../../../hooks/useCallApmApi'; import { Config } from '../index'; import { getOptionLabel } from '../../../../../../common/agent_configuration_constants'; -import { useKibanaCore } from '../../../../../../../observability/public'; import { APMClient } from '../../../../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; interface Props { onDeleted: () => void; @@ -21,10 +21,7 @@ interface Props { export function DeleteButton({ onDeleted, selectedConfig }: Props) { const [isDeleting, setIsDeleting] = useState(false); - const { - notifications: { toasts } - } = useKibanaCore(); - + const { toasts } = useApmPluginContext().core.notifications; const callApmApi = useCallApmApi(); return ( diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx index 853ce26d324fbc..e1cb07be3d3786 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -33,7 +33,7 @@ import { useFetcher } from '../../../../../hooks/useFetcher'; import { isRumAgentName } from '../../../../../../common/agent_name'; import { ALL_OPTION_VALUE } from '../../../../../../common/agent_configuration_constants'; import { saveConfig } from './saveConfig'; -import { useKibanaCore } from '../../../../../../../observability/public'; +import { useApmPluginContext } from '../../../../../hooks/useApmPluginContext'; const defaultSettings = { TRANSACTION_SAMPLE_RATE: '1.0', @@ -54,9 +54,7 @@ export function AddEditFlyout({ onDeleted, selectedConfig }: Props) { - const { - notifications: { toasts } - } = useKibanaCore(); + const { toasts } = useApmPluginContext().core.notifications; const [isSaving, setIsSaving] = useState(false); const callApmApiFromHook = useCallApmApi(); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index 7ced0b6fdd5662..ba68e1726d2b41 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -23,7 +23,7 @@ import { useFetcher } from '../../../../hooks/useFetcher'; import { useCallApmApi } from '../../../../hooks/useCallApmApi'; import { APMClient } from '../../../../services/rest/createCallApmApi'; import { clearCache } from '../../../../services/rest/callApi'; -import { useKibanaCore } from '../../../../../../observability/public'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; const APM_INDEX_LABELS = [ { @@ -86,9 +86,7 @@ async function saveApmIndices({ } export function ApmIndices() { - const { - notifications: { toasts } - } = useKibanaCore(); + const { toasts } = useApmPluginContext().core.notifications; const [apmIndices, setApmIndices] = useState>({}); const [isSaving, setIsSaving] = useState(false); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx index b77524fca050dc..fe58fc39c6cfa6 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TraceLink/__test__/TraceLink.test.tsx @@ -3,14 +3,17 @@ * 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 { render } from '@testing-library/react'; import { shallow } from 'enzyme'; -import * as urlParamsHooks from '../../../../hooks/useUrlParams'; -import * as hooks from '../../../../hooks/useFetcher'; +import React from 'react'; import { TraceLink } from '../'; +import * as hooks from '../../../../hooks/useFetcher'; +import * as urlParamsHooks from '../../../../hooks/useUrlParams'; +import { MockApmPluginContextWrapper } from '../../../../utils/testHelpers'; + +const renderOptions = { wrapper: MockApmPluginContextWrapper }; -jest.mock('../../Main/route_config/index.tsx', () => ({ +jest.mock('../../Main/route_config', () => ({ routes: [ { path: '/services/:serviceName/transactions/view', @@ -28,7 +31,7 @@ describe('TraceLink', () => { jest.clearAllMocks(); }); it('renders transition page', () => { - const component = render(); + const component = render(, renderOptions); expect(component.getByText('Fetching trace...')).toBeDefined(); }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx index a5356be72f5e40..882682f1f67606 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/__jest__/TransactionOverview.test.tsx @@ -18,19 +18,16 @@ import { history } from '../../../../utils/history'; import { TransactionOverview } from '..'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import * as useServiceTransactionTypesHook from '../../../../hooks/useServiceTransactionTypes'; +import * as useFetcherHook from '../../../../hooks/useFetcher'; import { fromQuery } from '../../../shared/Links/url_helpers'; import { Router } from 'react-router-dom'; import { UrlParamsProvider } from '../../../../context/UrlParamsContext'; -import { KibanaCoreContext } from '../../../../../../observability/public'; -import { LegacyCoreStart } from 'kibana/public'; +import { MockApmPluginContextWrapper } from '../../../../utils/testHelpers'; jest.spyOn(history, 'push'); jest.spyOn(history, 'replace'); -const coreMock = ({ - notifications: { toasts: { addWarning: () => {} } } -} as unknown) as LegacyCoreStart; - +jest.mock('ui/new_platform'); function setup({ urlParams, serviceTransactionTypes @@ -51,14 +48,16 @@ function setup({ .spyOn(useServiceTransactionTypesHook, 'useServiceTransactionTypes') .mockReturnValue(serviceTransactionTypes); + jest.spyOn(useFetcherHook, 'useFetcher').mockReturnValue({} as any); + return render( - + - + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx index f016052df56a2b..de356b5812e9a4 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -15,7 +15,6 @@ import { import { Location } from 'history'; import { first } from 'lodash'; import React, { useMemo } from 'react'; -import { useKibanaCore } from '../../../../../observability/public'; import { useTransactionList } from '../../../hooks/useTransactionList'; import { useTransactionCharts } from '../../../hooks/useTransactionCharts'; import { IUrlParams } from '../../../context/UrlParamsContext/types'; @@ -35,6 +34,7 @@ import { PROJECTION } from '../../../../common/projections/typings'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; function getRedirectLocation({ urlParams, @@ -86,7 +86,7 @@ export function TransactionOverview() { status: transactionListStatus } = useTransactionList(urlParams); - const { http } = useKibanaCore(); + const { http } = useApmPluginContext().core; const { data: hasMLJob = false } = useFetcher(() => { if (serviceName && transactionType) { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx index 05094c59712a92..32379325c40205 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/DatePicker/__test__/DatePicker.test.tsx @@ -10,13 +10,13 @@ import { UrlParamsContext, useUiFilters } from '../../../../context/UrlParamsContext'; -import { tick } from '../../../../utils/testHelpers'; import { DatePicker } from '../index'; import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { history } from '../../../../utils/history'; import { mount } from 'enzyme'; import { EuiSuperDatePicker } from '@elastic/eui'; import { MemoryRouter } from 'react-router-dom'; +import { wait } from '@testing-library/react'; const mockHistoryPush = jest.spyOn(history, 'push'); const mockRefreshTimeRange = jest.fn(); @@ -84,7 +84,7 @@ describe('DatePicker', () => { }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); - await tick(); + await wait(); expect(mockRefreshTimeRange).toHaveBeenCalled(); wrapper.unmount(); }); @@ -94,7 +94,7 @@ describe('DatePicker', () => { mountDatePicker({ refreshPaused: true, refreshInterval: 1000 }); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000); - await tick(); + await wait(); expect(mockRefreshTimeRange).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx index 32fbe46ac560c4..67bff86c8ccdfc 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/KueryBar/index.tsx @@ -15,7 +15,7 @@ import { getBoolFilter } from './get_bool_filter'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { history } from '../../../utils/history'; -import { usePlugins } from '../../../new-platform/plugin'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDynamicIndexPattern } from '../../../hooks/useDynamicIndexPattern'; import { AutocompleteProvider, @@ -71,7 +71,7 @@ export function KueryBar() { }); const { urlParams } = useUrlParams(); const location = useLocation(); - const { data } = usePlugins(); + const { data } = useApmPluginContext().plugins; const autocompleteProvider = data.autocomplete.getProvider('kuery'); let currentRequestCheck; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx index 5d25dc7de4e131..51d8b43dac0eaa 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/DiscoverLink.tsx @@ -10,8 +10,8 @@ import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData } from '../rison_helpers'; -import { useKibanaCore } from '../../../../../../observability/public'; import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; interface Props { query: { @@ -31,7 +31,7 @@ interface Props { } export function DiscoverLink({ query = {}, ...rest }: Props) { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const location = useLocation(); const risonQuery = { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx index 839efbbdf60f1d..2f49e476ef610b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/DiscoverLinks/__test__/DiscoverLinks.integration.test.tsx @@ -13,31 +13,8 @@ import { getRenderedHref } from '../../../../../utils/testHelpers'; import { DiscoverErrorLink } from '../DiscoverErrorLink'; import { DiscoverSpanLink } from '../DiscoverSpanLink'; import { DiscoverTransactionLink } from '../DiscoverTransactionLink'; -import * as kibanaCore from '../../../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; describe('DiscoverLinks', () => { - beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => null); - - const coreMock = ({ - http: { - notifications: { - toasts: {} - }, - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } - } as unknown) as LegacyCoreStart; - - spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock); - }); - - afterAll(() => { - jest.restoreAllMocks(); - }); - it('produces the correct URL for a transaction', async () => { const transaction = { transaction: { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 1888e1d04c2cb4..efae8982c22359 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; -import { usePlugins } from '../../../new-platform/plugin'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; // union type constisting of valid guide sections that we link to type DocsSection = '/apm/get-started' | '/x-pack' | '/apm/server'; @@ -17,8 +17,7 @@ interface Props extends EuiLinkAnchorProps { } export function ElasticDocsLink({ section, path, ...rest }: Props) { - const { apm } = usePlugins(); - const { stackVersion } = apm; - const href = `https://www.elastic.co/guide/en${section}/${stackVersion}${path}`; + const { version } = useApmPluginContext().packageInfo; + const href = `https://www.elastic.co/guide/en${section}/${version}${path}`; return ; } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx index 4f96f529c471c5..42022a37414953 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.test.tsx @@ -8,18 +8,6 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { InfraLink } from './InfraLink'; -import * as kibanaCore from '../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; - -const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } -} as unknown) as LegacyCoreStart; - -jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); test('InfraLink produces the correct URL', async () => { const href = await getRenderedHref( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx index 192fafadba4c01..8ff5e3010d6cc1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/InfraLink.tsx @@ -9,7 +9,7 @@ import { compact } from 'lodash'; import React from 'react'; import url from 'url'; import { fromQuery } from './url_helpers'; -import { useKibanaCore } from '../../../../../observability/public'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; interface InfraQueryParams { time?: number; @@ -24,7 +24,7 @@ interface Props extends EuiLinkAnchorProps { } export function InfraLink({ path, query = {}, ...rest }: Props) { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const nextSearch = fromQuery(query); const href = url.format({ pathname: core.http.basePath.prepend('/app/infra'), diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx index 521d62205311d2..fad534e11f645e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx @@ -8,26 +8,8 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../utils/testHelpers'; import { KibanaLink } from './KibanaLink'; -import * as kibanaCore from '../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; describe('KibanaLink', () => { - beforeEach(() => { - const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } - } as unknown) as LegacyCoreStart; - - jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - it('produces the correct URL', async () => { const href = await getRenderedHref(() => , { search: '?rangeFrom=now-5h&rangeTo=now-2h' diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx index de62d5e46070a0..37e2c06d2f701d 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/KibanaLink.tsx @@ -7,7 +7,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import React from 'react'; import url from 'url'; -import { useKibanaCore } from '../../../../../observability/public'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; interface Props extends EuiLinkAnchorProps { path?: string; @@ -15,7 +15,7 @@ interface Props extends EuiLinkAnchorProps { } export function KibanaLink({ path, ...rest }: Props) { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const href = url.format({ pathname: core.http.basePath.prepend('/app/kibana'), hash: path diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx index aaf27e75ce93b8..75a247a1aae40b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx @@ -8,25 +8,8 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLJobLink } from './MLJobLink'; -import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; describe('MLJobLink', () => { - beforeEach(() => { - const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } - } as unknown) as LegacyCoreStart; - - spyOn(kibanaCore, 'useKibanaCore').and.returnValue(coreMock); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); it('should produce the correct URL', async () => { const href = await getRenderedHref( () => ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx index 0db30e136b6ec6..d38d61fede37a7 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.test.tsx @@ -8,26 +8,6 @@ import { Location } from 'history'; import React from 'react'; import { getRenderedHref } from '../../../../utils/testHelpers'; import { MLLink } from './MLLink'; -import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; - -const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } -} as unknown) as LegacyCoreStart; - -jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); - -beforeAll(() => { - jest.spyOn(console, 'error').mockImplementation(() => null); -}); - -afterAll(() => { - jest.restoreAllMocks(); -}); test('MLLink produces the correct URL', async () => { const href = await getRenderedHref( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx index 0fe80b729f0104..3671a0089fd6e2 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLLink.tsx @@ -10,7 +10,7 @@ import url from 'url'; import rison, { RisonValue } from 'rison-node'; import { useLocation } from '../../../../hooks/useLocation'; import { getTimepickerRisonData, TimepickerRisonData } from '../rison_helpers'; -import { useKibanaCore } from '../../../../../../observability/public'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; interface MlRisonData { ml?: { @@ -25,7 +25,7 @@ interface Props { } export function MLLink({ children, path = '', query = {} }: Props) { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const location = useLocation(); const risonQuery: MlRisonData & TimepickerRisonData = getTimepickerRisonData( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx index c3913e43cbd62b..5bbc194e35992e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/__test__/ErrorMetadata.test.tsx @@ -11,11 +11,11 @@ import { APMError } from '../../../../../../typings/es_schemas/ui/APMError'; import { expectTextsInDocument, expectTextsNotInDocument, - MockPluginContextWrapper + MockApmPluginContextWrapper } from '../../../../../utils/testHelpers'; const renderOptions = { - wrapper: MockPluginContextWrapper + wrapper: MockApmPluginContextWrapper }; function getError() { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx index 438fe57218cc9b..4b6355034f16ae 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/__test__/SpanMetadata.test.tsx @@ -11,11 +11,11 @@ import { Span } from '../../../../../../typings/es_schemas/ui/Span'; import { expectTextsInDocument, expectTextsNotInDocument, - MockPluginContextWrapper + MockApmPluginContextWrapper } from '../../../../../utils/testHelpers'; const renderOptions = { - wrapper: MockPluginContextWrapper + wrapper: MockApmPluginContextWrapper }; describe('SpanMetadata', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx index 1ea20ecd645629..1fcb093fa03544 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/__test__/TransactionMetadata.test.tsx @@ -11,11 +11,11 @@ import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction import { expectTextsInDocument, expectTextsNotInDocument, - MockPluginContextWrapper + MockApmPluginContextWrapper } from '../../../../../utils/testHelpers'; const renderOptions = { - wrapper: MockPluginContextWrapper + wrapper: MockApmPluginContextWrapper }; function getTransaction() { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx index bed25fcc640127..979b9118a7534e 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/MetadataTable/__test__/MetadataTable.test.tsx @@ -9,12 +9,12 @@ import { render } from '@testing-library/react'; import { MetadataTable } from '..'; import { expectTextsInDocument, - MockPluginContextWrapper + MockApmPluginContextWrapper } from '../../../../utils/testHelpers'; import { SectionsWithRows } from '../helper'; const renderOptions = { - wrapper: MockPluginContextWrapper + wrapper: MockApmPluginContextWrapper }; describe('MetadataTable', () => { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx index 4a3b77b699c5f9..040d29aaa56dd4 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu.tsx @@ -23,7 +23,7 @@ import { DiscoverTransactionLink } from '../Links/DiscoverLinks/DiscoverTransact import { InfraLink } from '../Links/InfraLink'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { fromQuery } from '../Links/url_helpers'; -import { useKibanaCore } from '../../../../../observability/public'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; function getInfraMetricsQuery(transaction: Transaction) { const plus5 = new Date(transaction['@timestamp']); @@ -65,7 +65,7 @@ export const TransactionActionMenu: FunctionComponent = ( ) => { const { transaction } = props; - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const [isOpen, setIsOpen] = useState(false); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index 4bb018c760f1f6..2bfa5cf1274fae 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -9,12 +9,12 @@ import { render, fireEvent } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; import * as Transactions from './mockData'; -import * as kibanaCore from '../../../../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'src/core/public'; +import { MockApmPluginContextWrapper } from '../../../../utils/testHelpers'; const renderTransaction = async (transaction: Record) => { const rendered = render( - + , + { wrapper: MockApmPluginContextWrapper } ); fireEvent.click(rendered.getByText('Actions')); @@ -23,22 +23,6 @@ const renderTransaction = async (transaction: Record) => { }; describe('TransactionActionMenu component', () => { - beforeEach(() => { - const coreMock = ({ - http: { - basePath: { - prepend: (path: string) => `/basepath${path}` - } - } - } as unknown) as LegacyCoreStart; - - jest.spyOn(kibanaCore, 'useKibanaCore').mockReturnValue(coreMock); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - it('should always render the discover link', async () => { const { queryByText } = await renderTransaction( Transactions.transactionWithMinimalData diff --git a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx index e95f733fb4bc85..6d3e29ec099850 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx @@ -7,11 +7,18 @@ import React from 'react'; import { shallow } from 'enzyme'; import { BrowserLineChart } from './BrowserLineChart'; +import { MockApmPluginContextWrapper } from '../../../../utils/testHelpers'; describe('BrowserLineChart', () => { describe('render', () => { it('renders', () => { - expect(() => shallow()).not.toThrowError(); + expect(() => + shallow( + + + + ) + ).not.toThrowError(); }); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts index 798e872dbc4724..9048afe57153de 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/Delayed/index.ts @@ -57,4 +57,10 @@ export class Delayed { public onChange(onChangeCallback: Callback) { this.onChangeCallback = onChangeCallback; } + + public destroy() { + if (this.timeoutId) { + window.clearTimeout(this.timeoutId); + } + } } diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx index 57e634df22837e..c55c6ab3518482 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.test.tsx @@ -4,11 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { renderHook } from '@testing-library/react-hooks'; +import { + renderHook, + act, + RenderHookResult +} from '@testing-library/react-hooks'; import { useDelayedVisibility } from '.'; describe('useFetcher', () => { - let hook; + let hook: RenderHookResult; + beforeEach(() => { jest.useFakeTimers(); }); @@ -26,9 +31,15 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(10); + act(() => { + jest.advanceTimersByTime(10); + }); + expect(hook.result.current).toEqual(false); - jest.advanceTimersByTime(50); + act(() => { + jest.advanceTimersByTime(50); + }); + expect(hook.result.current).toEqual(true); }); @@ -38,8 +49,11 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(100); + act(() => { + jest.advanceTimersByTime(100); + }); hook.rerender(false); + expect(hook.result.current).toEqual(true); }); @@ -49,11 +63,22 @@ describe('useFetcher', () => { }); hook.rerender(true); - jest.advanceTimersByTime(100); + + act(() => { + jest.advanceTimersByTime(100); + }); + hook.rerender(false); - jest.advanceTimersByTime(900); + act(() => { + jest.advanceTimersByTime(900); + }); + expect(hook.result.current).toEqual(true); - jest.advanceTimersByTime(100); + + act(() => { + jest.advanceTimersByTime(100); + }); + expect(hook.result.current).toEqual(false); }); }); diff --git a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts index 5acbbd1d457372..c4465c7b42339a 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts +++ b/x-pack/legacy/plugins/apm/public/components/shared/useDelayedVisibility/index.ts @@ -26,6 +26,10 @@ export function useDelayedVisibility( setIsVisible(visibility); }); delayedRef.current = delayed; + + return () => { + delayed.destroy(); + }; }, [hideDelayMs, showDelayMs, minimumVisibleDuration]); useEffect(() => { diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx new file mode 100644 index 00000000000000..86efd9b31974e8 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext.tsx @@ -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 { createContext } from 'react'; +import { AppMountContext, PackageInfo } from 'kibana/public'; +import { ApmPluginSetupDeps, ConfigSchema } from '../new-platform/plugin'; + +export interface ApmPluginContextValue { + config: ConfigSchema; + core: AppMountContext['core']; + packageInfo: PackageInfo; + plugins: ApmPluginSetupDeps; +} + +export const ApmPluginContext = createContext({} as ApmPluginContextValue); diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx index 1c340f4b4f3c77..36e780f50c3ae3 100644 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx +++ b/x-pack/legacy/plugins/apm/public/context/LicenseContext/InvalidLicenseNotification.tsx @@ -6,10 +6,10 @@ import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { useKibanaCore } from '../../../../observability/public'; +import { useApmPluginContext } from '../../hooks/useApmPluginContext'; export function InvalidLicenseNotification() { - const core = useKibanaCore(); + const { core } = useApmPluginContext(); const manageLicenseURL = core.http.basePath.prepend( '/app/kibana#/management/elasticsearch/license_management' ); diff --git a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx index 38a402fd72ed20..4bb246a2a745ba 100644 --- a/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/LicenseContext/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FETCH_STATUS, useFetcher } from '../../hooks/useFetcher'; import { loadLicense, LicenseApiResponse } from '../../services/rest/xpack'; import { InvalidLicenseNotification } from './InvalidLicenseNotification'; -import { useKibanaCore } from '../../../../observability/public'; +import { useApmPluginContext } from '../../hooks/useApmPluginContext'; const initialLicense: LicenseApiResponse = { features: {}, @@ -18,7 +18,7 @@ const initialLicense: LicenseApiResponse = { export const LicenseContext = React.createContext(initialLicense); export const LicenseProvider: React.FC = ({ children }) => { - const { http } = useKibanaCore(); + const { http } = useApmPluginContext().core; const { data = initialLicense, status } = useFetcher( () => loadLicense(http), [http] diff --git a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx index 2604a3a1225746..d2d8036e864aee 100644 --- a/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx +++ b/x-pack/legacy/plugins/apm/public/context/UrlParamsContext/__tests__/UrlParamsContext.test.tsx @@ -11,8 +11,8 @@ import { Location, History } from 'history'; import { MemoryRouter, Router } from 'react-router-dom'; import moment from 'moment-timezone'; import { IUrlParams } from '../types'; -import { tick } from '../../../utils/testHelpers'; import { getParsedDate } from '../helpers'; +import { wait } from '@testing-library/react'; function mountParams(location: Location) { return mount( @@ -143,13 +143,13 @@ describe('UrlParamsContext', () => { ); - await tick(); + await wait(); expect(calls.length).toBe(1); wrapper.find('button').simulate('click'); - await tick(); + await wait(); expect(calls.length).toBe(2); @@ -194,11 +194,11 @@ describe('UrlParamsContext', () => { ); - await tick(); + await wait(); wrapper.find('button').simulate('click'); - await tick(); + await wait(); const params = getDataFromOutput(wrapper); expect(params.start).toEqual('2000-06-14T00:00:00.000Z'); diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js b/x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts similarity index 57% rename from x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js rename to x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts index 64b9a14f9c438a..80a04edbca8584 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_fields_routes.js +++ b/x-pack/legacy/plugins/apm/public/hooks/useApmPluginContext.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerListRoute } from './register_list_route'; +import { useContext } from 'react'; +import { ApmPluginContext } from '../context/ApmPluginContext'; -export function registerFieldsRoutes(server) { - registerListRoute(server); +export function useApmPluginContext() { + return useContext(ApmPluginContext); } diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts b/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts index 6b12fd04d09161..415e6172ae81e4 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useCallApi.ts @@ -5,11 +5,11 @@ */ import { useMemo } from 'react'; -import { useKibanaCore } from '../../../observability/public'; import { callApi, FetchOptions } from '../services/rest/callApi'; +import { useApmPluginContext } from './useApmPluginContext'; export function useCallApi() { - const { http } = useKibanaCore(); + const { http } = useApmPluginContext().core; return useMemo(() => { return (options: FetchOptions) => callApi(http, options); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts b/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts index b201b396e05c3a..b28b295d8189e4 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts +++ b/x-pack/legacy/plugins/apm/public/hooks/useCallApmApi.ts @@ -5,11 +5,11 @@ */ import { useMemo } from 'react'; -import { useKibanaCore } from '../../../observability/public'; import { createCallApmApi } from '../services/rest/createCallApmApi'; +import { useApmPluginContext } from './useApmPluginContext'; export function useCallApmApi() { - const { http } = useKibanaCore(); + const { http } = useApmPluginContext().core; return useMemo(() => { return createCallApmApi(http); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx index 36a8377c02527c..8d8716e6e5cd78 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.integration.test.tsx @@ -4,25 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { render, wait } from '@testing-library/react'; import React from 'react'; -import { render } from '@testing-library/react'; -import { delay, tick } from '../utils/testHelpers'; +import { delay, MockApmPluginContextWrapper } from '../utils/testHelpers'; import { useFetcher } from './useFetcher'; -import { KibanaCoreContext } from '../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'kibana/public'; - -// Wrap the hook with a provider so it can useKibanaCore -const wrapper = ({ children }: { children?: React.ReactNode }) => ( - {} } } - } as unknown) as LegacyCoreStart - } - > - {children} - -); + +const wrapper = MockApmPluginContextWrapper; async function asyncFn(name: string, ms: number) { await delay(ms); @@ -76,7 +63,8 @@ describe('when simulating race condition', () => { it('should render "Hello from Peter" after 200ms', async () => { jest.advanceTimersByTime(200); - await tick(); + + await wait(); expect(renderSpy).lastCalledWith({ data: 'Hello from Peter', @@ -87,7 +75,7 @@ describe('when simulating race condition', () => { it('should render "Hello from Peter" after 600ms', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(renderSpy).lastCalledWith({ data: 'Hello from Peter', @@ -98,7 +86,7 @@ describe('when simulating race condition', () => { it('should should NOT have rendered "Hello from John" at any point', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(renderSpy).not.toHaveBeenCalledWith({ data: 'Hello from John', @@ -109,7 +97,7 @@ describe('when simulating race condition', () => { it('should send and receive calls in the right order', async () => { jest.advanceTimersByTime(600); - await tick(); + await wait(); expect(requestCallOrder).toEqual([ ['request', 'John', 500], diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx index 92246499c62291..e3ef1d44c8b031 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx @@ -5,24 +5,11 @@ */ import { renderHook } from '@testing-library/react-hooks'; -import { delay } from '../utils/testHelpers'; +import { delay, MockApmPluginContextWrapper } from '../utils/testHelpers'; import { useFetcher } from './useFetcher'; -import { KibanaCoreContext } from '../../../observability/public/context/kibana_core'; -import { LegacyCoreStart } from 'kibana/public'; -import React from 'react'; - -// Wrap the hook with a provider so it can useKibanaCore -const wrapper = ({ children }: { children?: React.ReactNode }) => ( - {} } } - } as unknown) as LegacyCoreStart - } - > - {children} - -); + +// Wrap the hook with a provider so it can useApmPluginContext +const wrapper = MockApmPluginContextWrapper; describe('useFetcher', () => { describe('when resolving after 500ms', () => { diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index 2d60273c1896a8..ac8f40a29d93a8 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -10,9 +10,9 @@ import { IHttpFetchError } from 'src/core/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; import { LoadingIndicatorContext } from '../context/LoadingIndicatorContext'; import { useComponentId } from './useComponentId'; -import { useKibanaCore } from '../../../observability/public'; import { APMClient } from '../services/rest/createCallApmApi'; import { useCallApmApi } from './useCallApmApi'; +import { useApmPluginContext } from './useApmPluginContext'; export enum FETCH_STATUS { LOADING = 'loading', @@ -42,7 +42,7 @@ export function useFetcher( preservePreviousData?: boolean; } = {} ): Result> & { refetch: () => void } { - const { notifications } = useKibanaCore(); + const { notifications } = useApmPluginContext().core; const { preservePreviousData = true } = options; const id = useComponentId(); diff --git a/x-pack/legacy/plugins/apm/public/index.tsx b/x-pack/legacy/plugins/apm/public/index.tsx index db14e1c520020e..59b2fedaafba6c 100644 --- a/x-pack/legacy/plugins/apm/public/index.tsx +++ b/x-pack/legacy/plugins/apm/public/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { npStart } from 'ui/new_platform'; +import { npSetup, npStart } from 'ui/new_platform'; import 'react-vis/dist/style.css'; import { PluginInitializerContext } from 'kibana/public'; import 'ui/autoload/all'; @@ -14,8 +14,6 @@ import { REACT_APP_ROOT_ID } from './new-platform/plugin'; import './style/global_overrides.css'; import template from './templates/index.html'; -const { core, plugins } = npStart; - // This will be moved to core.application.register when the new platform // migration is complete. // @ts-ignore @@ -32,5 +30,7 @@ const checkForRoot = () => { }); }; checkForRoot().then(() => { - plugin({} as PluginInitializerContext).start(core, plugins); + const pluginInstance = plugin({} as PluginInitializerContext); + pluginInstance.setup(npSetup.core, npSetup.plugins); + pluginInstance.start(npStart.core, npStart.plugins); }); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index 8277707e538ac0..64784617442efa 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, createContext } from 'react'; +import React from 'react'; import ReactDOM from 'react-dom'; import { Router, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; +import { metadata } from 'ui/metadata'; import { HomePublicPluginSetup } from '../../../../../../src/plugins/home/public'; import { CoreStart, - LegacyCoreStart, Plugin, CoreSetup, - PluginInitializerContext + PluginInitializerContext, + PackageInfo } from '../../../../../../src/core/public'; -import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; -import { KibanaCoreContextProvider } from '../../../observability/public'; +import { DataPublicPluginSetup } from '../../../../../../src/plugins/data/public'; import { history } from '../utils/history'; import { LocationProvider } from '../context/LocationContext'; import { UrlParamsProvider } from '../context/UrlParamsContext'; @@ -25,7 +25,7 @@ import { px, unit, units } from '../style/variables'; import { LoadingIndicatorProvider } from '../context/LoadingIndicatorContext'; import { LicenseProvider } from '../context/LicenseContext'; import { UpdateBreadcrumbs } from '../components/app/Main/UpdateBreadcrumbs'; -import { getRoutes } from '../components/app/Main/route_config'; +import { routes } from '../components/app/Main/route_config'; import { ScrollToTopOnPathChange } from '../components/app/Main/ScrollToTopOnPathChange'; import { MatchedRouteProvider } from '../context/MatchedRouteContext'; import { createStaticIndexPattern } from '../services/rest/index_pattern'; @@ -34,8 +34,7 @@ import { setReadonlyBadge } from './updateBadge'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { getConfigFromInjectedMetadata } from './getConfigFromInjectedMetadata'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; -import { BreadcrumbRoute } from '../components/app/Main/ProvideBreadcrumbs'; -import { stackVersionFromLegacyMetadata } from './stackVersionFromLegacyMetadata'; +import { ApmPluginContext } from '../context/ApmPluginContext'; export const REACT_APP_ROOT_ID = 'react-apm-root'; @@ -45,7 +44,7 @@ const MainContainer = styled.main` height: 100%; `; -const App = ({ routes }: { routes: BreadcrumbRoute[] }) => { +const App = () => { return ( @@ -63,13 +62,10 @@ export type ApmPluginSetup = void; export type ApmPluginStart = void; export interface ApmPluginSetupDeps { + data: DataPublicPluginSetup; home: HomePublicPluginSetup; } -export interface ApmPluginStartDeps { - data: DataPublicPluginStart; -} - export interface ConfigSchema { indexPatternTitle: string; serviceMapEnabled: boolean; @@ -78,27 +74,14 @@ export interface ConfigSchema { }; } -// These are to be used until we switch over all our context handling to -// kibana_react -export const PluginsContext = createContext< - ApmPluginStartDeps & { apm: { config: ConfigSchema; stackVersion: string } } ->( - {} as ApmPluginStartDeps & { - apm: { config: ConfigSchema; stackVersion: string }; - } -); -export function usePlugins() { - return useContext(PluginsContext); -} - export class ApmPlugin - implements - Plugin< - ApmPluginSetup, - ApmPluginStart, - ApmPluginSetupDeps, - ApmPluginStartDeps - > { + implements Plugin { + // When we switch over from the old platform to new platform the plugins will + // be coming from setup instead of start, since that's where we do + // `core.application.register`. During the transitions we put plugins on an + // instance property so we can use it in start. + setupPlugins: ApmPluginSetupDeps = {} as ApmPluginSetupDeps; + constructor( // @ts-ignore Not using initializerContext now, but will be once NP // migration is complete. @@ -108,10 +91,12 @@ export class ApmPlugin // Take the DOM element as the constructor, so we can mount the app. public setup(_core: CoreSetup, plugins: ApmPluginSetupDeps) { plugins.home.featureCatalogue.register(featureCatalogueEntry); + this.setupPlugins = plugins; } - public start(core: CoreStart, plugins: ApmPluginStartDeps) { + public start(core: CoreStart) { const i18nCore = core.i18n; + const plugins = this.setupPlugins; // Once we're actually an NP plugin we'll get the config from the // initializerContext like: @@ -124,40 +109,41 @@ export class ApmPlugin // Once we're actually an NP plugin we'll get the package info from the // initializerContext like: // - // const stackVersion = this.initializerContext.env.packageInfo.branch + // const packageInfo = this.initializerContext.env.packageInfo // // Until then we use a shim to get it from legacy metadata: - const stackVersion = stackVersionFromLegacyMetadata; - - const pluginsForContext = { ...plugins, apm: { config, stackVersion } }; - - const routes = getRoutes(config); + const packageInfo = metadata as PackageInfo; // render APM feedback link in global help menu setHelpExtension(core); setReadonlyBadge(core); toggleAppLinkInNav(core, config); + const apmPluginContextValue = { + config, + core, + packageInfo, + plugins + }; + ReactDOM.render( - - - - - - - - - - - - - - - - - - - , + + + + + + + + + + + + + + + + + , document.getElementById(REACT_APP_ROOT_ID) ); diff --git a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx index b5cee4a78b01c1..0c8a7cbc17884d 100644 --- a/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/legacy/plugins/apm/public/utils/testHelpers.tsx @@ -11,19 +11,20 @@ import enzymeToJson from 'enzyme-to-json'; import { Location } from 'history'; import moment from 'moment'; import { Moment } from 'moment-timezone'; -import React, { FunctionComponent, ReactNode } from 'react'; +import React, { ReactNode } from 'react'; import { render, waitForElement } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router-dom'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { APMConfig } from '../../../../../plugins/apm/server'; import { LocationProvider } from '../context/LocationContext'; import { PromiseReturnType } from '../../typings/common'; import { ESFilter } from '../../typings/elasticsearch'; import { - PluginsContext, - ConfigSchema, - ApmPluginStartDeps -} from '../new-platform/plugin'; + ApmPluginContext, + ApmPluginContextValue +} from '../context/ApmPluginContext'; +import { ConfigSchema } from '../new-platform/plugin'; export function toJson(wrapper: ReactWrapper) { return enzymeToJson(wrapper, { @@ -51,14 +52,15 @@ export function mockMoment() { // Useful for getting the rendered href from any kind of link component export async function getRenderedHref(Component: React.FC, location: Location) { const el = render( - - - - - + + + + + + + ); - await tick(); await waitForElement(() => el.container.querySelector('a')); const a = el.container.querySelector('a'); @@ -74,9 +76,6 @@ export function delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -// Await this when you need to "flush" promises to immediately resolve or throw in tests -export const tick = () => new Promise(resolve => setImmediate(resolve, 0)); - export function expectTextsNotInDocument(output: any, texts: string[]) { texts.forEach(text => { try { @@ -185,22 +184,52 @@ export async function inspectSearchParams( export type SearchParamsMock = PromiseReturnType; -export const MockPluginContextWrapper: FunctionComponent<{}> = ({ - children +const mockCore = { + chrome: { + setBreadcrumbs: () => {} + }, + http: { + basePath: { + prepend: (path: string) => `/basepath${path}` + } + }, + notifications: { + toasts: { + addWarning: () => {} + } + } +}; + +const mockConfig: ConfigSchema = { + indexPatternTitle: 'apm-*', + serviceMapEnabled: false, + ui: { + enabled: false + } +}; + +export const mockApmPluginContextValue = { + config: mockConfig, + core: mockCore, + packageInfo: { version: '0' }, + plugins: {} +}; + +export function MockApmPluginContextWrapper({ + children, + value = {} as ApmPluginContextValue }: { children?: ReactNode; -}) => { + value?: ApmPluginContextValue; +}) { return ( - {children} - + ); -}; +} diff --git a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 9591dfdecbfef0..66f2a8d1ac79f2 100644 --- a/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/legacy/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -183,7 +183,7 @@ async function createOrUpdateUser(newUser: User) { async function createUser(newUser: User) { const user = await callKibana({ method: 'POST', - url: `/api/security/v1/users/${newUser.username}`, + url: `/internal/security/users/${newUser.username}`, data: { ...newUser, enabled: true, @@ -209,7 +209,7 @@ async function updateUser(existingUser: User, newUser: User) { // assign role to user await callKibana({ method: 'POST', - url: `/api/security/v1/users/${username}`, + url: `/internal/security/users/${username}`, data: { ...existingUser, roles: allRoles } }); @@ -219,7 +219,7 @@ async function updateUser(existingUser: User, newUser: User) { async function getUser(username: string) { try { return await callKibana({ - url: `/api/security/v1/users/${username}` + url: `/internal/security/users/${username}` }); } catch (e) { const err = e as AxiosError; diff --git a/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx b/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx index e525ea4be46e0d..8d2edf9c29e9ee 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/layouts/no_data.tsx @@ -6,29 +6,25 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; -interface LayoutProps { +interface LayoutProps extends RouteComponentProps { + children: React.ReactNode; title: string | React.ReactNode; actionSection?: React.ReactNode; - modalClosePath?: string; } -export const NoDataLayout: React.FC = withRouter( - ({ actionSection, title, modalClosePath, children, history }) => { - return ( - - - - {title}} - body={children} - actions={actionSection} - /> - - - - ); - } -) as any; +export const NoDataLayout = withRouter(({ actionSection, title, children }: LayoutProps) => ( + + + + {title}} + body={children} + actions={actionSection} + /> + + + +)); diff --git a/x-pack/legacy/plugins/beats_management/public/components/navigation/connected_link.tsx b/x-pack/legacy/plugins/beats_management/public/components/navigation/connected_link.tsx index 30d12c9ce10dee..947e22ee290895 100644 --- a/x-pack/legacy/plugins/beats_management/public/components/navigation/connected_link.tsx +++ b/x-pack/legacy/plugins/beats_management/public/components/navigation/connected_link.tsx @@ -6,22 +6,24 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; -import { Link, withRouter } from 'react-router-dom'; +import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; -export function ConnectedLinkComponent({ +interface ConnectedLinkComponent extends RouteComponentProps { + location: any; + path: string; + disabled: boolean; + query: any; + [key: string]: any; +} + +export const ConnectedLinkComponent = ({ location, path, query, disabled, children, ...props -}: { - location: any; - path: string; - disabled: boolean; - query: any; - [key: string]: any; -}) { +}: ConnectedLinkComponent) => { if (disabled) { return ; } @@ -36,6 +38,6 @@ export function ConnectedLinkComponent({ className={`euiLink euiLink--primary ${props.className || ''}`} /> ); -} +}; -export const ConnectedLink = withRouter(ConnectedLinkComponent); +export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx index 29581508d2ad50..71e9163fe22e7d 100644 --- a/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx +++ b/x-pack/legacy/plugins/beats_management/public/containers/with_url_state.tsx @@ -6,7 +6,7 @@ import { parse, stringify } from 'querystring'; import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; import { FlatObject } from '../frontend_types'; import { RendererFunction } from '../utils/typed_react'; @@ -22,9 +22,7 @@ export interface URLStateProps { ) => void; urlState: URLState; } -interface ComponentProps { - history: any; - match: any; +interface ComponentProps extends RouteComponentProps { children: RendererFunction>; } @@ -66,8 +64,8 @@ export class WithURLStateComponent extends React.Compon } const search: string = stringify({ - ...(pastState as any), - ...(newState as any), + ...pastState, + ...newState, }); const newLocation = { @@ -86,16 +84,12 @@ export class WithURLStateComponent extends React.Compon }); }; } -export const WithURLState = withRouter(WithURLStateComponent); +export const WithURLState = withRouter(WithURLStateComponent); -export function withUrlState( - UnwrappedComponent: React.ComponentType -): React.FC { - return (origProps: OP) => { - return ( - - {(URLProps: URLStateProps) => } - - ); - }; +export function withUrlState(UnwrappedComponent: React.ComponentType) { + return (origProps: OP) => ( + + {(URLProps: URLStateProps) => } + + ); } diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx similarity index 97% rename from x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx rename to x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx index d486440c1fd7db..e855a381413eb5 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/__tests__/workpad_telemetry.test.tsx +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.test.tsx @@ -10,9 +10,10 @@ import { withUnconnectedElementsLoadedTelemetry, WorkpadLoadedMetric, WorkpadLoadedWithErrorsMetric, -} from '../workpad_telemetry'; -import { METRIC_TYPE } from '../../../../lib/ui_metric'; +} from './workpad_telemetry'; +import { METRIC_TYPE } from '../../../lib/ui_metric'; +jest.mock('ui/new_platform'); const trackMetric = jest.fn(); const Component = withUnconnectedElementsLoadedTelemetry(() =>
, trackMetric); diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index ee7dd981009d97..4377f2cb4d53bf 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -5,7 +5,10 @@ */ import { flatten } from 'lodash'; +import moment from 'moment-timezone'; import chrome from 'ui/chrome'; +import { npStart } from 'ui/new_platform'; +import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunction, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; // @ts-ignore untyped local @@ -21,6 +24,26 @@ interface Arguments { timezone: string; } +/** + * This function parses a given time range containing date math + * and returns ISO dates. Parsing is done respecting the given time zone. + * @param timeRange time range to parse + * @param timeZone time zone to do the parsing in + */ +function parseDateMath(timeRange: TimeRange, timeZone: string) { + // the datemath plugin always parses dates by using the current default moment time zone. + // to use the configured time zone, we are switching just for the bounds calculation. + const defaultTimezone = moment().zoneName(); + moment.tz.setDefault(timeZone); + + const parsedRange = npStart.plugins.data.query.timefilter.timefilter.calculateBounds(timeRange); + + // reset default moment timezone + moment.tz.setDefault(defaultTimezone); + + return parsedRange; +} + export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Promise> { const { help, args: argHelp } = getFunctionHelp().timelion; @@ -64,8 +87,8 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr // workpad, if it exists. Otherwise fall back on the function args. const timeFilter = context.and.find(and => and.type === 'time'); const range = timeFilter - ? { from: timeFilter.from, to: timeFilter.to } - : { from: args.from, to: args.to }; + ? { min: timeFilter.from, max: timeFilter.to } + : parseDateMath({ from: args.from, to: args.to }, args.timezone); const body = { extended: { @@ -79,8 +102,8 @@ export function timelion(): ExpressionFunction<'timelion', Filter, Arguments, Pr }, sheet: [args.query], time: { - from: range.from, - to: range.to, + from: range.min, + to: range.max, interval: args.interval, timezone: args.timezone, }, diff --git a/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts b/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts deleted file mode 100644 index 3fe78befd2f507..00000000000000 --- a/x-pack/legacy/plugins/canvas/server/routes/custom_elements.ts +++ /dev/null @@ -1,192 +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 boom from 'boom'; -import { omit } from 'lodash'; -import { SavedObjectsClientContract } from 'src/core/server'; - -import { API_ROUTE_CUSTOM_ELEMENT, CUSTOM_ELEMENT_TYPE } from '../../common/lib/constants'; -import { getId } from '../../public/lib/get_id'; -// @ts-ignore Untyped Local -import { formatResponse as formatRes } from '../lib/format_response'; -import { CustomElement } from '../../types'; - -import { CoreSetup } from '../shim'; - -// Exclude ID attribute for the type used for SavedObjectClient -type CustomElementAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - -interface CustomElementRequestFacade { - getSavedObjectsClient: () => SavedObjectsClientContract; -} - -type CustomElementRequest = CustomElementRequestFacade & { - params: { - id: string; - }; - payload: CustomElement; -}; - -type FindCustomElementRequest = CustomElementRequestFacade & { - query: { - name: string; - page: number; - perPage: number; - }; -}; - -export function customElements( - route: CoreSetup['http']['route'], - elasticsearch: CoreSetup['elasticsearch'] -) { - // @ts-ignore: errors not on Cluster type - const { errors: esErrors } = elasticsearch.getCluster('data'); - - const routePrefix = API_ROUTE_CUSTOM_ELEMENT; - const formatResponse = formatRes(esErrors); - - const createCustomElement = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - - if (!req.payload) { - return Promise.reject(boom.badRequest('A custom element payload is required')); - } - - const now = new Date().toISOString(); - const { id, ...payload } = req.payload; - return savedObjectsClient.create( - CUSTOM_ELEMENT_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id: id || getId('custom-element') } - ); - }; - - const updateCustomElement = (req: CustomElementRequest, newPayload?: CustomElement) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - const payload = newPayload ? newPayload : req.payload; - - const now = new Date().toISOString(); - - return savedObjectsClient - .get(CUSTOM_ELEMENT_TYPE, id) - .then(element => { - // TODO: Using create with force over-write because of version conflict issues with update - return savedObjectsClient.create( - CUSTOM_ELEMENT_TYPE, - { - ...element.attributes, - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': element.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - }); - }; - - const deleteCustomElement = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - - return savedObjectsClient.delete(CUSTOM_ELEMENT_TYPE, id); - }; - - const findCustomElement = (req: FindCustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { name, page, perPage } = req.query; - - return savedObjectsClient.find({ - type: CUSTOM_ELEMENT_TYPE, - sortField: '@timestamp', - sortOrder: 'desc', - search: name ? `${name}* | ${name}` : '*', - searchFields: ['name'], - fields: ['id', 'name', 'displayName', 'help', 'image', 'content', '@created', '@timestamp'], - page, - perPage, - }); - }; - - const getCustomElementById = (req: CustomElementRequest) => { - const savedObjectsClient = req.getSavedObjectsClient(); - const { id } = req.params; - return savedObjectsClient.get(CUSTOM_ELEMENT_TYPE, id); - }; - - // get custom element by id - route({ - method: 'GET', - path: `${routePrefix}/{id}`, - handler: (req: CustomElementRequest) => - getCustomElementById(req) - .then(obj => ({ id: obj.id, ...obj.attributes })) - .then(formatResponse) - .catch(formatResponse), - }); - - // create custom element - route({ - method: 'POST', - path: routePrefix, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler: (req: CustomElementRequest) => - createCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // update custom element - route({ - method: 'PUT', - path: `${routePrefix}/{id}`, - // @ts-ignore config option missing on route method type - config: { payload: { allow: 'application/json', maxBytes: 26214400 } }, // 25MB payload limit - handler: (req: CustomElementRequest) => - updateCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // delete custom element - route({ - method: 'DELETE', - path: `${routePrefix}/{id}`, - handler: (req: CustomElementRequest) => - deleteCustomElement(req) - .then(() => ({ ok: true })) - .catch(formatResponse), - }); - - // find custom elements - route({ - method: 'GET', - path: `${routePrefix}/find`, - handler: (req: FindCustomElementRequest) => - findCustomElement(req) - .then(formatResponse) - .then(resp => { - return { - total: resp.total, - customElements: resp.saved_objects.map(hit => ({ id: hit.id, ...hit.attributes })), - }; - }) - .catch(() => { - return { - total: 0, - customElements: [], - }; - }), - }); -} diff --git a/x-pack/legacy/plugins/canvas/server/routes/index.ts b/x-pack/legacy/plugins/canvas/server/routes/index.ts index 515d5b5e895edf..2f6b706fc7edbc 100644 --- a/x-pack/legacy/plugins/canvas/server/routes/index.ts +++ b/x-pack/legacy/plugins/canvas/server/routes/index.ts @@ -5,12 +5,10 @@ */ import { esFields } from './es_fields'; -import { customElements } from './custom_elements'; import { shareableWorkpads } from './shareables'; import { CoreSetup } from '../shim'; export function routes(setup: CoreSetup): void { - customElements(setup.http.route, setup.elasticsearch); esFields(setup.http.route, setup.elasticsearch); shareableWorkpads(setup.http.route); } diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/types.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/types.ts index 08004f8ff8b459..191c0405d2e2d4 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/types.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/types.ts @@ -7,7 +7,7 @@ import { RefObject } from 'react'; // @ts-ignore Unlinked Webpack Type import ContainerStyle from 'types/interpreter'; -import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { SavedObject, SavedObjectAttributes } from 'src/core/public'; import { ElementPosition, CanvasPage, CanvasWorkpad, RendererSpec } from '../types'; diff --git a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json index f2fe456a0c6e2a..843fba30bb4899 100644 --- a/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json +++ b/x-pack/legacy/plugins/console_extensions/spec/overrides/sql.query.json @@ -18,6 +18,7 @@ "cbor", "smile" ] - } + }, + "template": "_sql?format=json\n{\n \"query\": \"\"\"\n SELECT * FROM \"${1:TABLE}\"\n \"\"\"\n}\n" } } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js index 37d1305d667bf0..5e893b7d9208c7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js @@ -6,7 +6,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; import { fatalError } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -34,15 +34,13 @@ import { FollowerIndexEdit, } from './sections'; -export class App extends Component { - static contextTypes = { - router: PropTypes.shape({ - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired - }).isRequired - }).isRequired - } +class AppComponent extends Component { + static propTypes = { + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + createHref: PropTypes.func.isRequired, + }).isRequired, + }; constructor(...args) { super(...args); @@ -99,8 +97,13 @@ export class App extends Component { } registerRouter() { - const { router } = this.context; - routing.reactRouter = router; + const { history, location } = this.props; + routing.reactRouter = { + history, + route: { + location, + }, + }; } render() { @@ -196,3 +199,5 @@ export class App extends Component { ); } } + +export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js index 74fdf3301bccaa..d1dfd7a2062892 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js @@ -7,6 +7,7 @@ import { updateFields, updateFormErrors } from './follower_index_form'; +jest.mock('ui/new_platform'); jest.mock('ui/indices', () => ({ INDEX_ILLEGAL_CHARACTERS_VISIBLE: [], })); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js index 43d1da3f242a2e..c7b4b56ad1240c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js @@ -8,6 +8,7 @@ import { reducer, initialState } from './api'; import { API_STATUS } from '../../constants'; import { apiRequestStart, apiRequestEnd, setApiError } from '../actions'; +jest.mock('ui/new_platform'); jest.mock('../../constants', () => ({ API_STATUS: { IDLE: 'idle', diff --git a/x-pack/legacy/plugins/file_upload/public/plugin.ts b/x-pack/legacy/plugins/file_upload/public/plugin.ts index cc9ebbfc15b397..53b292b02760fc 100644 --- a/x-pack/legacy/plugins/file_upload/public/plugin.ts +++ b/x-pack/legacy/plugins/file_upload/public/plugin.ts @@ -6,8 +6,6 @@ import { Plugin, CoreStart } from 'src/core/public'; // @ts-ignore -import { initResources } from './util/indexing_service'; -// @ts-ignore import { JsonUploadAndParse } from './components/json_upload_and_parse'; // @ts-ignore import { initServicesAndConstants } from './kibana_services'; diff --git a/x-pack/legacy/plugins/graph/index.js b/x-pack/legacy/plugins/graph/index.ts similarity index 83% rename from x-pack/legacy/plugins/graph/index.js rename to x-pack/legacy/plugins/graph/index.ts index 9ece9966b7da4e..601a239574e6b5 100644 --- a/x-pack/legacy/plugins/graph/index.js +++ b/x-pack/legacy/plugins/graph/index.ts @@ -7,11 +7,12 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; +// @ts-ignore import migrations from './migrations'; -import { initServer } from './server'; import mappings from './mappings.json'; +import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; -export function graph(kibana) { +export const graph: LegacyPluginInitializer = kibana => { return new kibana.Plugin({ id: 'graph', configPrefix: 'xpack.graph', @@ -26,17 +27,17 @@ export function graph(kibana) { main: 'plugins/graph/index', }, styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: ['plugins/graph/hacks/toggle_app_link_in_nav'], - home: ['plugins/graph/register_feature'], mappings, migrations, }, - config(Joi) { + config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string().valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']).default('configAndData'), + savePolicy: Joi.string() + .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) + .default('configAndData'), }).default(); }, @@ -45,7 +46,7 @@ export function graph(kibana) { const config = server.config(); return { graphSavePolicy: config.get('xpack.graph.savePolicy'), - canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls') + canEditDrillDownUrls: config.get('xpack.graph.canEditDrillDownUrls'), }; }); @@ -72,11 +73,9 @@ export function graph(kibana) { read: ['index-pattern', 'graph-workspace'], }, ui: [], - } - } + }, + }, }); - - initServer(server); }, }); -} +}; diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js deleted file mode 100644 index 444e68dd03520e..00000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.js +++ /dev/null @@ -1,67 +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 { SavedObjectProvider } from 'ui/saved_objects/saved_object'; -import { i18n } from '@kbn/i18n'; -import { - extractReferences, - injectReferences, -} from './saved_workspace_references'; - -export function SavedWorkspaceProvider(Private) { - // SavedWorkspace constructor. Usually you'd interact with an instance of this. - // ID is option, without it one will be generated on save. - const SavedObject = Private(SavedObjectProvider); - class SavedWorkspace extends SavedObject { - constructor(id) { - // Gives our SavedWorkspace the properties of a SavedObject - super ({ - type: SavedWorkspace.type, - mapping: SavedWorkspace.mapping, - searchSource: SavedWorkspace.searchsource, - extractReferences: extractReferences, - injectReferences: injectReferences, - - // if this is null/undefined then the SavedObject will be assigned the defaults - id: id, - - // default values that will get assigned if the doc is new - defaults: { - title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', { - defaultMessage: 'New Graph Workspace' - }), - numLinks: 0, - numVertices: 0, - wsState: '{}', - version: 1 - } - - }); - - // Overwrite the default getDisplayName function which uses type and which is not very - // user friendly for this object. - this.getDisplayName = function () { - return 'graph workspace'; - }; - } - - } //End of class - - SavedWorkspace.type = 'graph-workspace'; - - // if type:workspace has no mapping, we push this mapping into ES - SavedWorkspace.mapping = { - title: 'text', - description: 'text', - numLinks: 'integer', - numVertices: 'integer', - version: 'integer', - wsState: 'json' - }; - - SavedWorkspace.searchsource = false; - return SavedWorkspace; -} diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts new file mode 100644 index 00000000000000..bcde72a02f02ee --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObject, SavedObjectKibanaServices } from 'ui/saved_objects/types'; +import { createSavedObjectClass } from 'ui/saved_objects/saved_object'; +import { i18n } from '@kbn/i18n'; +import { extractReferences, injectReferences } from './saved_workspace_references'; + +export interface SavedWorkspace extends SavedObject { + wsState?: string; +} + +export function createSavedWorkspaceClass(services: SavedObjectKibanaServices) { + // SavedWorkspace constructor. Usually you'd interact with an instance of this. + // ID is option, without it one will be generated on save. + const SavedObjectClass = createSavedObjectClass(services); + class SavedWorkspaceClass extends SavedObjectClass { + public static type: string = 'graph-workspace'; + // if type:workspace has no mapping, we push this mapping into ES + public static mapping: Record = { + title: 'text', + description: 'text', + numLinks: 'integer', + numVertices: 'integer', + version: 'integer', + wsState: 'json', + }; + // Order these fields to the top, the rest are alphabetical + public static fieldOrder = ['title', 'description']; + public static searchSource = false; + + public wsState?: string; + + constructor(id: string) { + // Gives our SavedWorkspace the properties of a SavedObject + super({ + type: SavedWorkspaceClass.type, + mapping: SavedWorkspaceClass.mapping, + searchSource: SavedWorkspaceClass.searchSource, + extractReferences, + injectReferences, + // if this is null/undefined then the SavedObject will be assigned the defaults + id, + // default values that will get assigned if the doc is new + defaults: { + title: i18n.translate('xpack.graph.savedWorkspace.workspaceNameTitle', { + defaultMessage: 'New Graph Workspace', + }), + numLinks: 0, + numVertices: 0, + wsState: '{}', + version: 1, + }, + }); + } + // Overwrite the default getDisplayName function which uses type and which is not very + // user friendly for this object. + getDisplayName = () => { + return 'graph workspace'; + }; + } + return SavedWorkspaceClass; +} diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.ts similarity index 91% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.js rename to x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.ts index 01eb7f9ead1f0e..716520cb83aa13 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.test.ts @@ -5,6 +5,7 @@ */ import { extractReferences, injectReferences } from './saved_workspace_references'; +import { SavedWorkspace } from './saved_workspace'; describe('extractReferences', () => { test('extracts references from wsState', () => { @@ -19,6 +20,7 @@ describe('extractReferences', () => { }) ), }, + references: [], }; const updatedDoc = extractReferences(doc); expect(updatedDoc).toMatchInlineSnapshot(` @@ -48,6 +50,7 @@ Object { }) ), }, + references: [], }; expect(() => extractReferences(doc)).toThrowErrorMatchingInlineSnapshot( `"indexPattern attribute is missing in \\"wsState\\""` @@ -59,12 +62,12 @@ describe('injectReferences', () => { test('injects references into context', () => { const context = { id: '1', - foo: true, + title: 'test', wsState: JSON.stringify({ indexPatternRefName: 'indexPattern_0', bar: true, }), - }; + } as SavedWorkspace; const references = [ { name: 'indexPattern_0', @@ -75,8 +78,8 @@ describe('injectReferences', () => { injectReferences(context, references); expect(context).toMatchInlineSnapshot(` Object { - "foo": true, "id": "1", + "title": "test", "wsState": "{\\"bar\\":true,\\"indexPattern\\":\\"pattern*\\"}", } `); @@ -85,13 +88,13 @@ Object { test('skips when wsState is not a string', () => { const context = { id: '1', - foo: true, - }; + title: 'test', + } as SavedWorkspace; injectReferences(context, []); expect(context).toMatchInlineSnapshot(` Object { - "foo": true, "id": "1", + "title": "test", } `); }); @@ -100,7 +103,7 @@ Object { const context = { id: '1', wsState: JSON.stringify({ bar: true }), - }; + } as SavedWorkspace; injectReferences(context, []); expect(context).toMatchInlineSnapshot(` Object { @@ -116,7 +119,7 @@ Object { wsState: JSON.stringify({ indexPatternRefName: 'indexPattern_0', }), - }; + } as SavedWorkspace; expect(() => injectReferences(context, [])).toThrowErrorMatchingInlineSnapshot( `"Could not find reference \\"indexPattern_0\\""` ); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.ts similarity index 73% rename from x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.js rename to x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.ts index a1b4254685c40a..3a596b80686559 100644 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.js +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspace_references.ts @@ -4,9 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export function extractReferences({ attributes, references = [] }) { +import { SavedObjectAttributes, SavedObjectReference } from 'kibana/public'; +import { SavedWorkspace } from './saved_workspace'; + +export function extractReferences({ + attributes, + references = [], +}: { + attributes: SavedObjectAttributes; + references: SavedObjectReference[]; +}) { // For some reason, wsState comes in stringified 2x - const state = JSON.parse(JSON.parse(attributes.wsState)); + const state = JSON.parse(JSON.parse(String(attributes.wsState))); const { indexPattern } = state; if (!indexPattern) { throw new Error('indexPattern attribute is missing in "wsState"'); @@ -20,7 +29,7 @@ export function extractReferences({ attributes, references = [] }) { name: 'indexPattern_0', type: 'index-pattern', id: indexPattern, - } + }, ], attributes: { ...attributes, @@ -29,7 +38,7 @@ export function extractReferences({ attributes, references = [] }) { }; } -export function injectReferences(savedObject, references) { +export function injectReferences(savedObject: SavedWorkspace, references: SavedObjectReference[]) { // Skip if wsState is missing, at the time of development of this, there is no guarantee each // saved object has wsState. if (typeof savedObject.wsState !== 'string') { @@ -41,7 +50,9 @@ export function injectReferences(savedObject, references) { if (!state.indexPatternRefName) { return; } - const indexPatternReference = references.find(reference => reference.name === state.indexPatternRefName); + const indexPatternReference = references.find( + reference => reference.name === state.indexPatternRefName + ); if (!indexPatternReference) { // Throw an error as "indexPatternRefName" means the reference exists within // "references" and in this scenario we have bad data. diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js deleted file mode 100644 index 1fef4b7c38c075..00000000000000 --- a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.js +++ /dev/null @@ -1,90 +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 _ from 'lodash'; - -import chrome from 'ui/chrome'; -import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import { i18n } from '@kbn/i18n'; - -import { SavedWorkspaceProvider } from './saved_workspace'; - - -export function SavedWorkspacesProvider(kbnUrl, Private, Promise) { - const savedObjectsClient = Private(SavedObjectsClientProvider); - const SavedWorkspace = Private(SavedWorkspaceProvider); - - this.type = SavedWorkspace.type; - this.Class = SavedWorkspace; - - this.loaderProperties = { - name: 'Graph workspace', - noun: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspaceLabel', { - defaultMessage: 'Graph workspace' - }), - nouns: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspacesLabel', { - defaultMessage: 'Graph workspaces' - }) - }; - - // Returns a single dashboard by ID, should be the name of the workspace - this.get = function (id) { - // Returns a promise that contains a workspace which is a subclass of docSource - return (new SavedWorkspace(id)).init(); - }; - - this.urlFor = function (id) { - return chrome.addBasePath(kbnUrl.eval('/app/graph#/workspace/{{id}}', { id })); - }; - - this.delete = function (ids) { - ids = !_.isArray(ids) ? [ids] : ids; - return Promise.map(ids, function (id) { - return (new SavedWorkspace(id)).delete(); - }); - }; - - this.mapHits = function (hit) { - const source = hit.attributes; - source.id = hit.id; - source.url = this.urlFor(hit.id); - source.icon = 'fa-share-alt';// looks like a graph - return source; - }; - - this.find = function (searchString, size = 100) { - let body; - if (searchString) { - body = { - query: { - simple_query_string: { - query: searchString + '*', - fields: ['title^3', 'description'], - default_operator: 'AND' - } - } - }; - } else { - body = { query: { match_all: {} } }; - } - - return savedObjectsClient.find({ - type: SavedWorkspace.type, - search: searchString ? `${searchString}*` : undefined, - perPage: size, - searchFields: ['title^3', 'description'] - }) - .then(resp => { - return { - total: resp.total, - hits: resp.savedObjects.map((hit) => this.mapHits(hit)) - }; - }); - }; -} - -SavedObjectRegistryProvider.register(SavedWorkspacesProvider); diff --git a/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts new file mode 100644 index 00000000000000..e28bb60fb466bc --- /dev/null +++ b/x-pack/legacy/plugins/graph/public/angular/services/saved_workspaces.ts @@ -0,0 +1,76 @@ +/* + * 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 { npSetup, npStart } from 'ui/new_platform'; +import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; +import { i18n } from '@kbn/i18n'; + +import { createSavedWorkspaceClass } from './saved_workspace'; + +export function SavedWorkspacesProvider() { + const savedObjectsClient = npStart.core.savedObjects.client; + const services = { + savedObjectsClient, + indexPatterns: npStart.plugins.data.indexPatterns, + chrome: npStart.core.chrome, + overlays: npStart.core.overlays, + }; + + const SavedWorkspace = createSavedWorkspaceClass(services); + const urlFor = (id: string) => + npSetup.core.http.basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`); + const mapHits = (hit: { id: string; attributes: Record }) => { + const source = hit.attributes; + source.id = hit.id; + source.url = urlFor(hit.id); + source.icon = 'fa-share-alt'; // looks like a graph + return source; + }; + + return { + type: SavedWorkspace.type, + Class: SavedWorkspace, + loaderProperties: { + name: 'Graph workspace', + noun: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspaceLabel', { + defaultMessage: 'Graph workspace', + }), + nouns: i18n.translate('xpack.graph.savedWorkspaces.graphWorkspacesLabel', { + defaultMessage: 'Graph workspaces', + }), + }, + // Returns a single dashboard by ID, should be the name of the workspace + get: (id: string) => { + // Returns a promise that contains a workspace which is a subclass of docSource + // @ts-ignore + return new SavedWorkspace(id).init(); + }, + urlFor, + delete: (ids: string | string[]) => { + const idArr = Array.isArray(ids) ? ids : [ids]; + return Promise.all( + idArr.map((id: string) => savedObjectsClient.delete(SavedWorkspace.type, id)) + ); + }, + find: (searchString: string, size: number = 100) => { + return savedObjectsClient + .find({ + type: SavedWorkspace.type, + search: searchString ? `${searchString}*` : undefined, + perPage: size, + searchFields: ['title^3', 'description'], + }) + .then(resp => { + return { + total: resp.total, + hits: resp.savedObjects.map(hit => mapHits(hit)), + }; + }); + }, + }; +} + +SavedObjectRegistryProvider.register(SavedWorkspacesProvider); diff --git a/x-pack/legacy/plugins/graph/public/app.js b/x-pack/legacy/plugins/graph/public/app.js index aa08841e03f524..353123524335b2 100644 --- a/x-pack/legacy/plugins/graph/public/app.js +++ b/x-pack/legacy/plugins/graph/public/app.js @@ -13,7 +13,6 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; -import { addAppRedirectMessageToUrl } from 'ui/notify'; import appTemplate from './angular/templates/index.html'; import listingTemplate from './angular/templates/listing_ng_wrapper.html'; @@ -39,10 +38,10 @@ import { hasFieldsSelector } from './state_management'; import { formatHttpError } from './helpers/format_http_error'; +import { checkLicense } from '../../../../plugins/graph/common/check_license'; export function initGraphApp(angularModule, deps) { const { - xpackInfo, chrome, savedGraphWorkspaces, toastNotifications, @@ -63,17 +62,6 @@ export function initGraphApp(angularModule, deps) { const app = angularModule; - function checkLicense(kbnBaseUrl) { - const licenseAllowsToShowThisPage = xpackInfo.get('features.graph.showAppLink') && - xpackInfo.get('features.graph.enableAppLink'); - if (!licenseAllowsToShowThisPage) { - const message = xpackInfo.get('features.graph.message'); - const newUrl = addAppRedirectMessageToUrl(addBasePath(kbnBaseUrl), message); - window.location.href = newUrl; - throw new Error('Graph license error'); - } - } - app.directive('vennDiagram', function (reactDirective) { return reactDirective(VennDiagram); }); @@ -123,7 +111,6 @@ export function initGraphApp(angularModule, deps) { template: listingTemplate, badge: getReadonlyBadge, controller($location, $scope) { - checkLicense(kbnBaseUrl); const services = savedObjectRegistry.byLoaderPropertiesName; const graphService = services['Graph workspace']; @@ -164,7 +151,6 @@ export function initGraphApp(angularModule, deps) { ) : savedGraphWorkspaces.get(); }, - //Copied from example found in wizard.js ( Kibana TODO - can't indexPatterns: function () { return savedObjectsClient.find({ type: 'index-pattern', @@ -185,10 +171,8 @@ export function initGraphApp(angularModule, deps) { //======== Controller for basic UI ================== app.controller('graphuiPlugin', function ($scope, $route, $location, confirmModal) { - checkLicense(kbnBaseUrl); function handleError(err) { - checkLicense(kbnBaseUrl); const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { defaultMessage: 'Graph Error', description: '"Graph" is a product name and should not be translated.', @@ -206,7 +190,6 @@ export function initGraphApp(angularModule, deps) { } async function handleHttpError(error) { - checkLicense(kbnBaseUrl); toastNotifications.addDanger(formatHttpError(error)); } diff --git a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js b/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js deleted file mode 100644 index 5b1eb6181fd610..00000000000000 --- a/x-pack/legacy/plugins/graph/public/hacks/toggle_app_link_in_nav.js +++ /dev/null @@ -1,19 +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 { npStart } from 'ui/new_platform'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; - -const navLinkUpdates = {}; -navLinkUpdates.hidden = true; -const showAppLink = xpackInfo.get('features.graph.showAppLink', false); -navLinkUpdates.hidden = !showAppLink; -if (showAppLink) { - navLinkUpdates.disabled = !xpackInfo.get('features.graph.enableAppLink', false); - navLinkUpdates.tooltip = xpackInfo.get('features.graph.message'); -} - -npStart.core.chrome.navLinks.update('graph', navLinkUpdates); diff --git a/x-pack/legacy/plugins/graph/public/index.ts b/x-pack/legacy/plugins/graph/public/index.ts index 712d08c106425e..600df6d3097847 100644 --- a/x-pack/legacy/plugins/graph/public/index.ts +++ b/x-pack/legacy/plugins/graph/public/index.ts @@ -12,13 +12,12 @@ import 'ui/autoload/all'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; // @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -// @ts-ignore import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { npSetup, npStart } from 'ui/new_platform'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { start as navigation } from '../../../../../src/legacy/core_plugins/navigation/public/legacy'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; import { GraphPlugin } from './plugin'; // @ts-ignore @@ -41,13 +40,17 @@ async function getAngularInjectedDependencies(): Promise { const instance = new GraphPlugin(); instance.setup(npSetup.core, { __LEGACY: { - xpackInfo, Storage, }, + ...(npSetup.plugins as XpackNpSetupDeps), }); instance.start(npStart.core, { npData: npStart.plugins.data, diff --git a/x-pack/legacy/plugins/graph/public/plugin.ts b/x-pack/legacy/plugins/graph/public/plugin.ts index 13887bd163c422..1646cf200e3744 100644 --- a/x-pack/legacy/plugins/graph/public/plugin.ts +++ b/x-pack/legacy/plugins/graph/public/plugin.ts @@ -9,6 +9,7 @@ import { CoreSetup, CoreStart, Plugin, SavedObjectsClientContract } from 'src/co import { Plugin as DataPlugin } from 'src/plugins/data/public'; import { LegacyAngularInjectedDependencies } from './render_app'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; export interface GraphPluginStartDependencies { npData: ReturnType; @@ -18,8 +19,8 @@ export interface GraphPluginStartDependencies { export interface GraphPluginSetupDependencies { __LEGACY: { Storage: any; - xpackInfo: any; }; + licensing: LicensingPluginSetup; } export interface GraphPluginStartDependencies { @@ -34,7 +35,7 @@ export class GraphPlugin implements Plugin { private savedObjectsClient: SavedObjectsClientContract | null = null; private angularDependencies: LegacyAngularInjectedDependencies | null = null; - setup(core: CoreSetup, { __LEGACY: { xpackInfo, Storage } }: GraphPluginSetupDependencies) { + setup(core: CoreSetup, { __LEGACY: { Storage }, licensing }: GraphPluginSetupDependencies) { core.application.register({ id: 'graph', title: 'Graph', @@ -42,10 +43,10 @@ export class GraphPlugin implements Plugin { const { renderApp } = await import('./render_app'); return renderApp({ ...params, + licensing, navigation: this.navigationStart!, npData: this.npDataStart!, savedObjectsClient: this.savedObjectsClient!, - xpackInfo, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, canEditDrillDownUrls: core.injectedMetadata.getInjectedVar( diff --git a/x-pack/legacy/plugins/graph/public/register_feature.js b/x-pack/legacy/plugins/graph/public/register_feature.js deleted file mode 100644 index b3e2efc990b78a..00000000000000 --- a/x-pack/legacy/plugins/graph/public/register_feature.js +++ /dev/null @@ -1,25 +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 { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; - -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'graph', - title: 'Graph', - description: i18n.translate('xpack.graph.pluginDescription', { - defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.', - }), - icon: 'graphApp', - path: '/app/graph', - showOnHomePage: true, - category: FeatureCatalogueCategory.DATA - }; -}); diff --git a/x-pack/legacy/plugins/graph/public/render_app.ts b/x-pack/legacy/plugins/graph/public/render_app.ts index 0f3c52d38a01cd..b07a91e6d63286 100644 --- a/x-pack/legacy/plugins/graph/public/render_app.ts +++ b/x-pack/legacy/plugins/graph/public/render_app.ts @@ -19,6 +19,8 @@ import { configureAppAngularModule } from 'ui/legacy_compat'; import { createTopNavDirective, createTopNavHelper } from 'ui/kbn_top_nav/kbn_top_nav'; // @ts-ignore import { confirmModalFactory } from 'ui/modals/confirm_modal'; +// @ts-ignore +import { addAppRedirectMessageToUrl } from 'ui/notify'; // type imports import { @@ -36,6 +38,8 @@ import { IndexPatternsContract, } from '../../../../../src/plugins/data/public'; import { NavigationStart } from '../../../../../src/legacy/core_plugins/navigation/public'; +import { LicensingPluginSetup } from '../../../../plugins/licensing/public'; +import { checkLicense } from '../../../../plugins/graph/common/check_license'; /** * These are dependencies of the Graph app besides the base dependencies @@ -49,13 +53,13 @@ export interface GraphDependencies extends LegacyAngularInjectedDependencies { capabilities: Record>; coreStart: AppMountContext['core']; navigation: NavigationStart; + licensing: LicensingPluginSetup; chrome: ChromeStart; config: IUiSettingsClient; toastNotifications: ToastsStart; indexPatterns: IndexPatternsContract; npData: ReturnType; savedObjectsClient: SavedObjectsClientContract; - xpackInfo: { get(path: string): unknown }; addBasePath: (url: string) => string; getBasePath: () => string; Storage: any; @@ -82,9 +86,23 @@ export interface LegacyAngularInjectedDependencies { export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { const graphAngularModule = createLocalAngularModule(deps.navigation); configureAppAngularModule(graphAngularModule, deps.coreStart as LegacyCoreStart, true); + + const licenseSubscription = deps.licensing.license$.subscribe(license => { + const info = checkLicense(license); + const licenseAllowsToShowThisPage = info.showAppLink && info.enableAppLink; + + if (!licenseAllowsToShowThisPage) { + const newUrl = addAppRedirectMessageToUrl(deps.addBasePath(deps.kbnBaseUrl), info.message); + window.location.href = newUrl; + } + }); + initGraphApp(graphAngularModule, deps); const $injector = mountGraphApp(appBasePath, element); - return () => $injector.get('$rootScope').$destroy(); + return () => { + licenseSubscription.unsubscribe(); + $injector.get('$rootScope').$destroy(); + }; }; const mainTemplate = (basePath: string) => `
diff --git a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts index 0a0fc8cae5d269..3bfc868fcb06e9 100644 --- a/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts +++ b/x-pack/legacy/plugins/graph/public/services/fetch_top_nodes.test.ts @@ -12,7 +12,7 @@ const icon = getSuitableIcon(''); describe('fetch_top_nodes', () => { it('should build terms agg', async () => { const postMock = jest.fn(() => Promise.resolve({ resp: {} })); - await fetchTopNodes(postMock, 'test', [ + await fetchTopNodes(postMock as any, 'test', [ { color: '', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, { color: '', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, ]); @@ -64,7 +64,7 @@ describe('fetch_top_nodes', () => { }, }) ); - const result = await fetchTopNodes(postMock, 'test', [ + const result = await fetchTopNodes(postMock as any, 'test', [ { color: 'red', hopSize: 5, icon, name: 'field1', selected: false, type: 'string' }, { color: 'blue', hopSize: 5, icon, name: 'field2', selected: false, type: 'string' }, ]); diff --git a/x-pack/legacy/plugins/graph/public/types/persistence.ts b/x-pack/legacy/plugins/graph/public/types/persistence.ts index 9c59b7057fe670..7883e81fb9b8e6 100644 --- a/x-pack/legacy/plugins/graph/public/types/persistence.ts +++ b/x-pack/legacy/plugins/graph/public/types/persistence.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from 'ui/saved_objects/saved_object'; +import { SavedObject } from 'ui/saved_objects/types'; import { AdvancedSettings, UrlTemplate, WorkspaceField } from './app_state'; import { WorkspaceNode, WorkspaceEdge } from './workspace_state'; diff --git a/x-pack/legacy/plugins/graph/server/init_server.js b/x-pack/legacy/plugins/graph/server/init_server.js deleted file mode 100644 index 4647fe9c2662a0..00000000000000 --- a/x-pack/legacy/plugins/graph/server/init_server.js +++ /dev/null @@ -1,26 +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 Boom from 'boom'; - -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { checkLicense } from './lib'; -import { graphExploreRoute, searchProxyRoute } from './routes'; - -export function initServer(server) { - const graphPlugin = server.plugins.graph; - const xpackMainPlugin = server.plugins.xpack_main; - - mirrorPluginStatus(xpackMainPlugin, graphPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results - xpackMainPlugin.info.feature('graph').registerLicenseCheckResultsGenerator(checkLicense); - }); - - server.route(graphExploreRoute); - server.route(searchProxyRoute); -} diff --git a/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js b/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js deleted file mode 100644 index c341dfbc378ca7..00000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/__tests__/check_license.js +++ /dev/null @@ -1,120 +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 expect from '@kbn/expect'; -import { set } from 'lodash'; -import sinon from 'sinon'; -import { checkLicense } from '../check_license'; - -describe('check_license: ', function () { - - let mockLicenseInfo; - let licenseCheckResult; - - beforeEach(() => { - mockLicenseInfo = { - isAvailable: () => true - }; - }); - - describe('mockLicenseInfo is not set', () => { - beforeEach(() => { - mockLicenseInfo = null; - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - describe('mockLicenseInfo is set but not available', () => { - beforeEach(() => { - mockLicenseInfo = { isAvailable: () => false }; - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - describe('graph is disabled in Elasticsearch', () => { - beforeEach(() => { - set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => false })); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to false', () => { - expect(licenseCheckResult.showAppLink).to.be(false); - }); - }); - - describe('graph is enabled in Elasticsearch', () => { - beforeEach(() => { - set(mockLicenseInfo, 'feature', sinon.stub().withArgs('graph').returns({ isEnabled: () => true })); - }); - - describe('& license is trial or platinum', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', sinon.stub().withArgs([ 'trial', 'platinum' ]).returns(true)); - set(mockLicenseInfo, 'license.getType', () => 'trial'); - }); - - describe('& license is active', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isActive', () => true); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to true', () => { - expect(licenseCheckResult.enableAppLink).to.be(true); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isActive', () => false); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to true', () => { - expect(licenseCheckResult.showAppLink).to.be(true); - }); - - it ('should set enableAppLink to false', () => { - expect(licenseCheckResult.enableAppLink).to.be(false); - }); - }); - - }); - - describe('& license is neither trial nor platinum', () => { - beforeEach(() => { - set(mockLicenseInfo, 'license.isOneOf', () => false); - set(mockLicenseInfo, 'license.getType', () => 'basic'); - set(mockLicenseInfo, 'license.isActive', () => true); - licenseCheckResult = checkLicense(mockLicenseInfo); - }); - - it ('should set showAppLink to false', () => { - expect(licenseCheckResult.showAppLink).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/graph/server/lib/check_license.js b/x-pack/legacy/plugins/graph/server/lib/check_license.js deleted file mode 100644 index 4ddac989a789af..00000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/check_license.js +++ /dev/null @@ -1,60 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - showAppLink: true, - enableAppLink: false, - message: i18n.translate('xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage', { - defaultMessage: 'Graph is unavailable - license information is not available at this time.', - }) - }; - } - - const graphFeature = xpackLicenseInfo.feature('graph'); - if (!graphFeature.isEnabled()) { - return { - showAppLink: false, - enableAppLink: false, - message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', { - defaultMessage: 'Graph is unavailable', - }) - }; - } - - const isLicenseActive = xpackLicenseInfo.license.isActive(); - let message; - if (!isLicenseActive) { - message = i18n.translate('xpack.graph.serverSideErrors.expiredLicenseErrorMessage', { - defaultMessage: 'Graph is unavailable - license has expired.', - }); - } - - if (xpackLicenseInfo.license.isOneOf([ 'trial', 'platinum' ])) { - return { - showAppLink: true, - enableAppLink: isLicenseActive, - message - }; - } - - message = i18n.translate('xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage', { - defaultMessage: 'Graph is unavailable for the current {licenseType} license. Please upgrade your license.', - values: { - licenseType: xpackLicenseInfo.license.getType(), - }, - }); - - return { - showAppLink: false, - enableAppLink: false, - message - }; -} diff --git a/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js b/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js deleted file mode 100644 index a656a4349f61f4..00000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/es/call_es_graph_explore_api.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; -import { get } from 'lodash'; - -export async function callEsGraphExploreApi({ callCluster, index, query }) { - try { - return { - ok: true, - resp: await callCluster('transport.request', { - 'path': '/' + encodeURIComponent(index) + '/_graph/explore', - body: query, - method: 'POST', - query: {} - }) - }; - } catch (error) { - // Extract known reasons for bad choice of field - const relevantCause = [].concat(get(error, 'body.error.root_cause', []) || []) - .find(cause => { - return ( - cause.reason.includes('Fielddata is disabled on text fields') || - cause.reason.includes('No support for examining floating point') || - cause.reason.includes('Sample diversifying key must be a single valued-field') || - cause.reason.includes('Failed to parse query') || - cause.type == 'parsing_exception' - ); - }); - - if (relevantCause) { - throw Boom.badRequest(relevantCause.reason); - } - - throw Boom.boomify(error); - } -} diff --git a/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js b/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js deleted file mode 100644 index cdc355d2bdea79..00000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/es/call_es_search_api.js +++ /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 Boom from 'boom'; - -export async function callEsSearchApi({ callCluster, index, body, queryParams }) { - try { - return { - ok: true, - resp: await callCluster('search', { - ...queryParams, - index, - body - }) - }; - } catch (error) { - throw Boom.boomify(error, { statusCode: error.statusCode || 500 }); - } -} diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js b/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js deleted file mode 100644 index a26e7247d62879..00000000000000 --- a/x-pack/legacy/plugins/graph/server/lib/pre/verify_api_access_pre.js +++ /dev/null @@ -1,19 +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 Boom from 'boom'; - -export function verifyApiAccessPre(request, h) { - const xpackInfo = request.server.plugins.xpack_main.info; - const graph = xpackInfo.feature('graph'); - const licenseCheckResults = graph.getLicenseCheckResults(); - - if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { - return null; - } else { - throw Boom.forbidden(licenseCheckResults.message); - } -} diff --git a/x-pack/legacy/plugins/graph/server/routes/graph_explore.js b/x-pack/legacy/plugins/graph/server/routes/graph_explore.js deleted file mode 100644 index 7f77903dd70506..00000000000000 --- a/x-pack/legacy/plugins/graph/server/routes/graph_explore.js +++ /dev/null @@ -1,37 +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 Joi from 'joi'; - -import { - verifyApiAccessPre, - getCallClusterPre, - callEsGraphExploreApi, -} from '../lib'; - -export const graphExploreRoute = { - path: '/api/graph/graphExplore', - method: 'POST', - config: { - pre: [ - verifyApiAccessPre, - getCallClusterPre, - ], - validate: { - payload: Joi.object().keys({ - index: Joi.string().required(), - query: Joi.object().required().unknown(true) - }).default() - }, - handler(request) { - return callEsGraphExploreApi({ - callCluster: request.pre.callCluster, - index: request.payload.index, - query: request.payload.query, - }); - } - } -}; diff --git a/x-pack/legacy/plugins/graph/server/routes/search_proxy.js b/x-pack/legacy/plugins/graph/server/routes/search_proxy.js deleted file mode 100644 index 64fc44b1e36771..00000000000000 --- a/x-pack/legacy/plugins/graph/server/routes/search_proxy.js +++ /dev/null @@ -1,43 +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 Joi from 'joi'; -import Boom from 'boom'; - -import { - verifyApiAccessPre, - getCallClusterPre, - callEsSearchApi, -} from '../lib'; - -export const searchProxyRoute = { - path: '/api/graph/searchProxy', - method: 'POST', - config: { - pre: [ - getCallClusterPre, - verifyApiAccessPre, - ], - validate: { - payload: Joi.object().keys({ - index: Joi.string().required(), - body: Joi.object().unknown(true).default() - }).default() - }, - async handler(request) { - const includeFrozen = await request.getUiSettingsService().get('search:includeFrozen'); - return await callEsSearchApi({ - callCluster: request.pre.callCluster, - index: request.payload.index, - body: request.payload.body, - queryParams: { - rest_total_hits_as_int: true, - ignore_throttled: !includeFrozen, - } - }); - } - } -}; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js index a97296e94042de..059a6a6f8221a5 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.js @@ -33,6 +33,8 @@ import { maximumDocumentsRequiredMessage, } from '../../public/store/selectors/lifecycle'; +jest.mock('ui/new_platform'); + let server; let store; const policy = { diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js index 31d8337857911b..121f7d3bcda0d6 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/components/policy_table.test.js @@ -17,6 +17,7 @@ import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { setHttpClient } from '../../public/services/api'; setHttpClient(axios.create({ adapter: axiosXhrAdapter })); import sinon from 'sinon'; +jest.mock('ui/new_platform'); let server = null; let store = null; diff --git a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js index ed5069a56141c7..25a76b5db28f6b 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/__jest__/extend_index_management.test.js @@ -14,6 +14,8 @@ import { ilmFilterExtension, ilmSummaryExtension, } from '../public/extend_index_management'; + +jest.mock('ui/new_platform'); const indexWithoutLifecyclePolicy = { health: 'yellow', status: 'open', diff --git a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.test.js b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.test.js index ab100db97d70c7..1fe1d0ccf6afaf 100644 --- a/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.test.js +++ b/x-pack/legacy/plugins/index_lifecycle_management/public/services/ui_metric.test.js @@ -21,6 +21,7 @@ import { } from '../constants'; import { getUiMetricsForPhases } from './ui_metric'; +jest.mock('ui/new_platform'); describe('getUiMetricsForPhases', () => { test('gets cold phase', () => { diff --git a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts index 37b037214bb02e..1728cd1fa4b45d 100644 --- a/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts +++ b/x-pack/legacy/plugins/infra/common/ecs_allowed_list.ts @@ -45,6 +45,8 @@ export const DOCKER_ALLOWED_LIST = [ 'docker.container.labels', ]; +export const AWS_S3_ALLOWED_LIST = ['aws.s3']; + export const getAllowedListForPrefix = (prefix: string) => { const firstPart = first(prefix.split(/\./)); const defaultAllowedList = prefix ? [...ECS_ALLOWED_LIST, prefix] : ECS_ALLOWED_LIST; @@ -55,6 +57,10 @@ export const getAllowedListForPrefix = (prefix: string) => { return [...defaultAllowedList, ...PROMETHEUS_ALLOWED_LIST]; case 'kubernetes': return [...defaultAllowedList, ...K8S_ALLOWED_LIST]; + case 'aws': + if (prefix === 'aws.s3_daily_storage') { + return [...defaultAllowedList, ...AWS_S3_ALLOWED_LIST]; + } default: return defaultAllowedList; } diff --git a/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts b/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts index fd86e605b8747e..071313817eff31 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/shared/schema.gql.ts @@ -30,5 +30,9 @@ export const sharedSchema = gql` pod container host + awsEC2 + awsS3 + awsRDS + awsSQS } `; diff --git a/x-pack/legacy/plugins/infra/common/graphql/types.ts b/x-pack/legacy/plugins/infra/common/graphql/types.ts index 273cbfc1d4f89a..0520409800bce4 100644 --- a/x-pack/legacy/plugins/infra/common/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/common/graphql/types.ts @@ -552,6 +552,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS', } export enum InfraSnapshotMetricType { @@ -562,6 +566,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsOldestMessage', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -602,6 +622,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts index ace61e13193c8d..7fc3c3e876f086 100644 --- a/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts +++ b/x-pack/legacy/plugins/infra/common/http_api/metadata_api.ts @@ -5,16 +5,11 @@ */ import * as rt from 'io-ts'; - -export const InfraMetadataNodeTypeRT = rt.keyof({ - host: null, - pod: null, - container: null, -}); +import { ItemTypeRT } from '../../common/inventory_models/types'; export const InfraMetadataRequestRT = rt.type({ nodeId: rt.string, - nodeType: InfraMetadataNodeTypeRT, + nodeType: ItemTypeRT, sourceId: rt.string, }); @@ -96,5 +91,3 @@ export type InfraMetadataMachine = rt.TypeOf; export type InfraMetadataHost = rt.TypeOf; export type InfraMetadataOS = rt.TypeOf; - -export type InfraMetadataNodeType = rt.TypeOf; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts new file mode 100644 index 00000000000000..ba4a6bb22c184d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsEC2: InventoryModel = { + id: 'awsEC2', + displayName: i18n.translate('xpack.infra.inventoryModels.awsEC2.displayName', { + defaultMessage: 'EC2 Instances', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + metrics, + fields: { + id: 'cloud.instance.id', + name: 'cloud.instance.name', + ip: 'aws.ec2.instance.public.ip', + }, + requiredMetrics: ['awsEC2CpuUtilization', 'awsEC2NetworkTraffic', 'awsEC2DiskIOBytes'], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx new file mode 100644 index 00000000000000..01009b478951a8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/layout.tsx @@ -0,0 +1,116 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.ts new file mode 100644 index 00000000000000..18b7cca2048a59 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/index.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 { cpu } from './snapshot/cpu'; +import { rx } from './snapshot/rx'; +import { tx } from './snapshot/tx'; +import { diskIOReadBytes } from './snapshot/disk_io_read_bytes'; +import { diskIOWriteBytes } from './snapshot/disk_io_write_bytes'; + +import { awsEC2CpuUtilization } from './tsvb/aws_ec2_cpu_utilization'; +import { awsEC2NetworkTraffic } from './tsvb/aws_ec2_network_traffic'; +import { awsEC2DiskIOBytes } from './tsvb/aws_ec2_diskio_bytes'; + +import { InventoryMetrics } from '../../types'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsEC2CpuUtilization, + awsEC2NetworkTraffic, + awsEC2DiskIOBytes, + }, + snapshot: { cpu, rx, tx, diskIOReadBytes, diskIOWriteBytes }, + defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.ts new file mode 100644 index 00000000000000..483d9de784919d --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/cpu.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. + */ + +import { SnapshotModel } from '../../../types'; + +export const cpu: SnapshotModel = { + cpu_avg: { + avg: { + field: 'aws.ec2.cpu.total.pct', + }, + }, + cpu: { + bucket_script: { + buckets_path: { + cpu: 'cpu_avg', + }, + script: { + source: 'params.cpu / 100', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts new file mode 100644 index 00000000000000..48e4a9eb59fadc --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_read_bytes.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const diskIOReadBytes: SnapshotModel = { + diskIOReadBytes: { + avg: { + field: 'aws.ec2.diskio.read.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts new file mode 100644 index 00000000000000..deadaa8c4a7768 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/disk_io_write_bytes.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const diskIOWriteBytes: SnapshotModel = { + diskIOWriteBytes: { + avg: { + field: 'aws.ec2.diskio.write.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts new file mode 100644 index 00000000000000..2b857ce9b338a0 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/rx.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const rx: SnapshotModel = { + rx: { + avg: { + field: 'aws.ec2.network.in.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts new file mode 100644 index 00000000000000..63c9da8ea18882 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/snapshot/tx.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const tx: SnapshotModel = { + tx: { + avg: { + field: 'aws.ec2.network.in.bytes_per_sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts new file mode 100644 index 00000000000000..a7a06ef1cfc1d4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_cpu_utilization.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsEC2CpuUtilization = createTSVBModel( + 'awsEC2CpuUtilization', + ['aws.ec2'], + [ + { + id: 'total', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.cpu.total.pct', + id: 'avg-cpu', + type: 'avg', + }, + { + id: 'convert-to-percent', + script: 'params.avg / 100', + type: 'calculation', + variables: [ + { + field: 'avg-cpu', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts new file mode 100644 index 00000000000000..35d165936211ab --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_diskio_bytes.ts @@ -0,0 +1,41 @@ +/* + * 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 { createTSVBModel } from '../../../create_tsvb_model'; +export const awsEC2DiskIOBytes = createTSVBModel( + 'awsEC2DiskIOBytes', + ['aws.ec2'], + [ + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.diskio.write.bytes_per_sec', + id: 'avg-write', + type: 'avg', + }, + ], + }, + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.diskio.read.bytes_per_sec', + id: 'avg-read', + type: 'avg', + }, + { + id: 'calculation-rate', + type: 'calculation', + variables: [{ id: 'rate-var', name: 'rate', field: 'avg-read' }], + script: 'params.rate * -1', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts new file mode 100644 index 00000000000000..ea4b41d0bcd685 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/metrics/tsvb/aws_ec2_network_traffic.ts @@ -0,0 +1,41 @@ +/* + * 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 { createTSVBModel } from '../../../create_tsvb_model'; +export const awsEC2NetworkTraffic = createTSVBModel( + 'awsEC2NetworkTraffic', + ['aws.ec2'], + [ + { + id: 'tx', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.network.out.bytes_per_sec', + id: 'avg-tx', + type: 'avg', + }, + ], + }, + { + id: 'rx', + split_mode: 'everything', + metrics: [ + { + field: 'aws.ec2.network.in.bytes_per_sec', + id: 'avg-rx', + type: 'avg', + }, + { + id: 'calculation-rate', + type: 'calculation', + variables: [{ id: 'rate-var', name: 'rate', field: 'avg-rx' }], + script: 'params.rate * -1', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx new file mode 100644 index 00000000000000..fc09f0761a5221 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_ec2/toolbar_items.tsx @@ -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 React from 'react'; +import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; + +export const AwsEC2ToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + InfraSnapshotMetricType.diskIOReadBytes, + InfraSnapshotMetricType.diskIOWriteBytes, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'aws.ec2.instance.image.id', + 'aws.ec2.instance.state.name', + ]; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts new file mode 100644 index 00000000000000..e81dee504b0649 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsRDS: InventoryModel = { + id: 'awsRDS', + displayName: i18n.translate('xpack.infra.inventoryModels.awsRDS.displayName', { + defaultMessage: 'RDS Databases', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.rds.db_instance.arn', + name: 'aws.rds.db_instance.identifier', + }, + requiredMetrics: [ + 'awsRDSCpuTotal', + 'awsRDSConnections', + 'awsRDSQueriesExecuted', + 'awsRDSActiveTransactions', + 'awsRDSLatency', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx new file mode 100644 index 00000000000000..5f1185666a35dc --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/layout.tsx @@ -0,0 +1,180 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts new file mode 100644 index 00000000000000..eaded5d8df223f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/index.ts @@ -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 { InventoryMetrics } from '../../types'; + +import { cpu } from './snapshot/cpu'; +import { rdsLatency } from './snapshot/rds_latency'; +import { rdsConnections } from './snapshot/rds_connections'; +import { rdsQueriesExecuted } from './snapshot/rds_queries_executed'; +import { rdsActiveTransactions } from './snapshot/rds_active_transactions'; + +import { awsRDSLatency } from './tsvb/aws_rds_latency'; +import { awsRDSConnections } from './tsvb/aws_rds_connections'; +import { awsRDSCpuTotal } from './tsvb/aws_rds_cpu_total'; +import { awsRDSQueriesExecuted } from './tsvb/aws_rds_queries_executed'; +import { awsRDSActiveTransactions } from './tsvb/aws_rds_active_transactions'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsRDSLatency, + awsRDSConnections, + awsRDSCpuTotal, + awsRDSQueriesExecuted, + awsRDSActiveTransactions, + }, + snapshot: { + cpu, + rdsLatency, + rdsConnections, + rdsQueriesExecuted, + rdsActiveTransactions, + }, + defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.ts new file mode 100644 index 00000000000000..e277b3b11958b7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/cpu.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. + */ + +import { SnapshotModel } from '../../../types'; + +export const cpu: SnapshotModel = { + cpu_avg: { + avg: { + field: 'aws.rds.cpu.total.pct', + }, + }, + cpu: { + bucket_script: { + buckets_path: { + cpu: 'cpu_avg', + }, + script: { + source: 'params.cpu / 100', + lang: 'painless', + }, + gap_policy: 'skip', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts new file mode 100644 index 00000000000000..be3dba100ba299 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_active_transactions.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const rdsActiveTransactions: SnapshotModel = { + rdsActiveTransactions: { + avg: { + field: 'aws.rds.transactions.active', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts new file mode 100644 index 00000000000000..c7855d5548eeab --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_connections.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const rdsConnections: SnapshotModel = { + rdsConnections: { + avg: { + field: 'aws.rds.database_connections', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts new file mode 100644 index 00000000000000..2997b54d2f92e9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_latency.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const rdsLatency: SnapshotModel = { + rdsLatency: { + avg: { + field: 'aws.rds.latency.dml', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts new file mode 100644 index 00000000000000..18e6538fb1e1e3 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/snapshot/rds_queries_executed.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const rdsQueriesExecuted: SnapshotModel = { + rdsQueriesExecuted: { + avg: { + field: 'aws.rds.queries', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.ts new file mode 100644 index 00000000000000..026cdeac40c361 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_active_transactions.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSActiveTransactions = createTSVBModel( + 'awsRDSActiveTransactions', + ['aws.rds'], + [ + { + id: 'active', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.transactions.active', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'blocked', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.transactions.blocked', + id: 'avg', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts new file mode 100644 index 00000000000000..145cc758e4a5b9 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_connections.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSConnections = createTSVBModel( + 'awsRDSConnections', + ['aws.rds'], + [ + { + id: 'connections', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.database_connections', + id: 'avg-conns', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts new file mode 100644 index 00000000000000..9a8eefc859bb01 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_cpu_total.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSCpuTotal = createTSVBModel( + 'awsRDSCpuTotal', + ['aws.rds'], + [ + { + id: 'cpu', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.cpu.total.pct', + id: 'avg-cpu', + type: 'avg', + }, + { + id: 'convert-to-percent', + script: 'params.avg / 100', + type: 'calculation', + variables: [ + { + field: 'avg-cpu', + id: 'var-avg', + name: 'avg', + }, + ], + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts new file mode 100644 index 00000000000000..80dffeeb717c66 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_latency.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsRDSLatency = createTSVBModel( + 'awsRDSLatency', + ['aws.rds'], + [ + { + id: 'read', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.read', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'write', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.write', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'insert', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.insert', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'update', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.update', + id: 'avg', + type: 'avg', + }, + ], + }, + { + id: 'commit', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.latency.commit', + id: 'avg', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.ts new file mode 100644 index 00000000000000..4dd1a1e89a21a6 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/metrics/tsvb/aws_rds_queries_executed.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 { createTSVBModel } from '../../../create_tsvb_model'; +export const awsRDSQueriesExecuted = createTSVBModel( + 'awsRDSQueriesExecuted', + ['aws.rds'], + [ + { + id: 'queries', + split_mode: 'everything', + metrics: [ + { + field: 'aws.rds.queries', + id: 'avg-queries', + type: 'avg', + }, + ], + }, + ] +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx new file mode 100644 index 00000000000000..b60d6292c9c0f5 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_rds/toolbar_items.tsx @@ -0,0 +1,32 @@ +/* + * 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 { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { InfraSnapshotMetricType } from '../../../public/graphql/types'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; + +export const AwsRDSToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.rdsConnections, + InfraSnapshotMetricType.rdsQueriesExecuted, + InfraSnapshotMetricType.rdsActiveTransactions, + InfraSnapshotMetricType.rdsLatency, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'aws.rds.db_instance.class', + 'aws.rds.db_instance.status', + ]; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts new file mode 100644 index 00000000000000..c5de4ed80a1cb8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsS3: InventoryModel = { + id: 'awsS3', + displayName: i18n.translate('xpack.infra.inventoryModels.awsS3.displayName', { + defaultMessage: 'S3 Buckets', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.s3.bucket.name', + name: 'aws.s3.bucket.name', + }, + requiredMetrics: [ + 'awsS3BucketSize', + 'awsS3NumberOfObjects', + 'awsS3TotalRequests', + 'awsS3DownloadBytes', + 'awsS3UploadBytes', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx new file mode 100644 index 00000000000000..80089f15b04b2b --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/layout.tsx @@ -0,0 +1,143 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts new file mode 100644 index 00000000000000..5aa974c16feecc --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/index.ts @@ -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 { InventoryMetrics } from '../../types'; + +import { awsS3BucketSize } from './tsvb/aws_s3_bucket_size'; +import { awsS3TotalRequests } from './tsvb/aws_s3_total_requests'; +import { awsS3NumberOfObjects } from './tsvb/aws_s3_number_of_objects'; +import { awsS3DownloadBytes } from './tsvb/aws_s3_download_bytes'; +import { awsS3UploadBytes } from './tsvb/aws_s3_upload_bytes'; + +import { s3BucketSize } from './snapshot/s3_bucket_size'; +import { s3TotalRequests } from './snapshot/s3_total_requests'; +import { s3NumberOfObjects } from './snapshot/s3_number_of_objects'; +import { s3DownloadBytes } from './snapshot/s3_download_bytes'; +import { s3UploadBytes } from './snapshot/s3_upload_bytes'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsS3BucketSize, + awsS3TotalRequests, + awsS3NumberOfObjects, + awsS3DownloadBytes, + awsS3UploadBytes, + }, + snapshot: { + s3BucketSize, + s3NumberOfObjects, + s3TotalRequests, + s3UploadBytes, + s3DownloadBytes, + }, + defaultSnapshot: 's3BucketSize', + defaultTimeRangeInSeconds: 86400 * 7, // 7 days +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts new file mode 100644 index 00000000000000..a99753a39c97ce --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_bucket_size.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const s3BucketSize: SnapshotModel = { + s3BucketSize: { + max: { + field: 'aws.s3_daily_storage.bucket.size.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts new file mode 100644 index 00000000000000..a0b23dadee37aa --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_download_bytes.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const s3DownloadBytes: SnapshotModel = { + s3DownloadBytes: { + max: { + field: 'aws.s3_request.downloaded.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts new file mode 100644 index 00000000000000..29162a59db47a8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_number_of_objects.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const s3NumberOfObjects: SnapshotModel = { + s3NumberOfObjects: { + max: { + field: 'aws.s3_daily_storage.number_of_objects', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts new file mode 100644 index 00000000000000..bc57c6eb382348 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_total_requests.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const s3TotalRequests: SnapshotModel = { + s3TotalRequests: { + max: { + field: 'aws.s3_request.requests.total', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts new file mode 100644 index 00000000000000..977d73254c3cd4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/snapshot/s3_upload_bytes.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const s3UploadBytes: SnapshotModel = { + s3UploadBytes: { + max: { + field: 'aws.s3_request.uploaded.bytes', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.ts new file mode 100644 index 00000000000000..216f98b9e16b4e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_bucket_size.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3BucketSize = createTSVBModel( + 'awsS3BucketSize', + ['aws.s3_daily_storage'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_daily_storage.bucket.size.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=86400s', + false +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts new file mode 100644 index 00000000000000..15eb3130a5e232 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_download_bytes.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3DownloadBytes = createTSVBModel( + 'awsS3DownloadBytes', + ['aws.s3_request'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.downloaded.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.ts new file mode 100644 index 00000000000000..c108735bc0efdf --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_number_of_objects.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3NumberOfObjects = createTSVBModel( + 'awsS3NumberOfObjects', + ['aws.s3_daily_storage'], + [ + { + id: 'objects', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_daily_storage.number_of_objects', + id: 'max-size', + type: 'max', + }, + ], + }, + ], + '>=86400s', + false +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.ts new file mode 100644 index 00000000000000..311067fd96b47e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_total_requests.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3TotalRequests = createTSVBModel( + 'awsS3TotalRequests', + ['aws.s3_request'], + [ + { + id: 'total', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.requests.total', + id: 'max-size', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.ts new file mode 100644 index 00000000000000..ab66b47cfa7814 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/metrics/tsvb/aws_s3_upload_bytes.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsS3UploadBytes = createTSVBModel( + 'awsS3UploadBytes', + ['aws.s3_request'], + [ + { + id: 'bytes', + split_mode: 'everything', + metrics: [ + { + field: 'aws.s3_request.uploaded.bytes', + id: 'max-bytes', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.tsx new file mode 100644 index 00000000000000..6764de237118ab --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_s3/toolbar_items.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 { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { InfraSnapshotMetricType } from '../../../public/graphql/types'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; + +export const AwsS3ToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.s3BucketSize, + InfraSnapshotMetricType.s3NumberOfObjects, + InfraSnapshotMetricType.s3TotalRequests, + InfraSnapshotMetricType.s3DownloadBytes, + InfraSnapshotMetricType.s3UploadBytes, + ]; + const groupByFields = ['cloud.region']; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts new file mode 100644 index 00000000000000..d7fb7c7a615b14 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/index.ts @@ -0,0 +1,35 @@ +/* + * 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 { metrics } from './metrics'; +import { InventoryModel } from '../types'; + +export const awsSQS: InventoryModel = { + id: 'awsSQS', + displayName: i18n.translate('xpack.infra.inventoryModels.awsSQS.displayName', { + defaultMessage: 'SQS Queues', + }), + requiredModules: ['aws'], + crosslinkSupport: { + details: true, + logs: true, + apm: false, + uptime: false, + }, + metrics, + fields: { + id: 'aws.sqs.queue.name', + name: 'aws.sqs.queue.name', + }, + requiredMetrics: [ + 'awsSQSMessagesVisible', + 'awsSQSMessagesDelayed', + 'awsSQSMessagesSent', + 'awsSQSMessagesEmpty', + 'awsSQSOldestMessage', + ], +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx new file mode 100644 index 00000000000000..40cb0a64d83cc5 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/layout.tsx @@ -0,0 +1,143 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { LayoutPropsWithTheme } from '../../../public/pages/metrics/types'; +import { Section } from '../../../public/pages/metrics/components/section'; +import { SubSection } from '../../../public/pages/metrics/components/sub_section'; +import { ChartSectionVis } from '../../../public/pages/metrics/components/chart_section_vis'; +import { withTheme } from '../../../../../common/eui_styled_components'; + +export const Layout = withTheme(({ metrics, theme }: LayoutPropsWithTheme) => ( + +
+ + + + + + + + + + + + + + + +
+
+)); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts new file mode 100644 index 00000000000000..7bc593cc220356 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/index.ts @@ -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 { InventoryMetrics } from '../../types'; + +import { sqsMessagesVisible } from './snapshot/sqs_messages_visible'; +import { sqsMessagesDelayed } from './snapshot/sqs_messages_delayed'; +import { sqsMessagesEmpty } from './snapshot/sqs_messages_empty'; +import { sqsMessagesSent } from './snapshot/sqs_messages_sent'; +import { sqsOldestMessage } from './snapshot/sqs_oldest_message'; + +import { awsSQSMessagesVisible } from './tsvb/aws_sqs_messages_visible'; +import { awsSQSMessagesDelayed } from './tsvb/aws_sqs_messages_delayed'; +import { awsSQSMessagesSent } from './tsvb/aws_sqs_messages_sent'; +import { awsSQSMessagesEmpty } from './tsvb/aws_sqs_messages_empty'; +import { awsSQSOldestMessage } from './tsvb/aws_sqs_oldest_message'; + +export const metrics: InventoryMetrics = { + tsvb: { + awsSQSMessagesVisible, + awsSQSMessagesDelayed, + awsSQSMessagesSent, + awsSQSMessagesEmpty, + awsSQSOldestMessage, + }, + snapshot: { + sqsMessagesVisible, + sqsMessagesDelayed, + sqsMessagesEmpty, + sqsMessagesSent, + sqsOldestMessage, + }, + defaultSnapshot: 'sqsMessagesVisible', + defaultTimeRangeInSeconds: 14400, // 4 hours +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts new file mode 100644 index 00000000000000..679f86671725e7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_delayed.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const sqsMessagesDelayed: SnapshotModel = { + sqsMessagesDelayed: { + max: { + field: 'aws.sqs.messages.delayed', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts new file mode 100644 index 00000000000000..d80a3f3451e1d8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_empty.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const sqsMessagesEmpty: SnapshotModel = { + sqsMessagesEmpty: { + max: { + field: 'aws.sqs.messages.not_visible', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts new file mode 100644 index 00000000000000..3d6934bf3da85f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_sent.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const sqsMessagesSent: SnapshotModel = { + sqsMessagesSent: { + max: { + field: 'aws.sqs.messages.sent', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts new file mode 100644 index 00000000000000..1a78c50cd7949f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_messages_visible.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const sqsMessagesVisible: SnapshotModel = { + sqsMessagesVisible: { + avg: { + field: 'aws.sqs.messages.visible', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts new file mode 100644 index 00000000000000..ae780069c8ca1f --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/snapshot/sqs_oldest_message.ts @@ -0,0 +1,15 @@ +/* + * 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 { SnapshotModel } from '../../../types'; + +export const sqsOldestMessage: SnapshotModel = { + sqsOldestMessage: { + max: { + field: 'aws.sqs.oldest_message_age.sec', + }, + }, +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.ts new file mode 100644 index 00000000000000..469b9ddd339532 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_delayed.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesDelayed = createTSVBModel( + 'awsSQSMessagesDelayed', + ['aws.sqs'], + [ + { + id: 'delayed', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.delayed', + id: 'avg-delayed', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.ts new file mode 100644 index 00000000000000..54c9e503a8c8c7 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_empty.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesEmpty = createTSVBModel( + 'awsSQSMessagesEmpty', + ['aws.sqs'], + [ + { + id: 'empty', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.not_visible', + id: 'avg-empty', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.ts new file mode 100644 index 00000000000000..98389ef22fbe8e --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_sent.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesSent = createTSVBModel( + 'awsSQSMessagesSent', + ['aws.sqs'], + [ + { + id: 'sent', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.sent', + id: 'avg-sent', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.ts new file mode 100644 index 00000000000000..c96ab07e4ae754 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_messages_visible.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSMessagesVisible = createTSVBModel( + 'awsSQSMessagesVisible', + ['aws.sqs'], + [ + { + id: 'visible', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.messages.visible', + id: 'avg-visible', + type: 'avg', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.ts new file mode 100644 index 00000000000000..812906386fb679 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/metrics/tsvb/aws_sqs_oldest_message.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 { createTSVBModel } from '../../../create_tsvb_model'; + +export const awsSQSOldestMessage = createTSVBModel( + 'awsSQSOldestMessage', + ['aws.sqs'], + [ + { + id: 'oldest', + split_mode: 'everything', + metrics: [ + { + field: 'aws.sqs.oldest_message_age.sec', + id: 'max-oldest', + type: 'max', + }, + ], + }, + ], + '>=300s' +); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.tsx new file mode 100644 index 00000000000000..89d372d6ac21c2 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/aws_sqs/toolbar_items.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 { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; + +export const AwsSQSToolbarItems = (props: ToolbarProps) => { + const metricTypes = [ + InfraSnapshotMetricType.sqsMessagesVisible, + InfraSnapshotMetricType.sqsMessagesDelayed, + InfraSnapshotMetricType.sqsMessagesSent, + InfraSnapshotMetricType.sqsMessagesEmpty, + InfraSnapshotMetricType.sqsOldestMessage, + ]; + const groupByFields = ['cloud.region']; + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts index 54fe938528d199..af7b6058ff1745 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/index.ts @@ -4,12 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; export const container: InventoryModel = { id: 'container', + displayName: i18n.translate('xpack.infra.inventoryModel.container.displayName', { + defaultMessage: 'Docker Containers', + }), requiredModules: ['docker'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'container.id', + name: 'container.name', + ip: 'continaer.ip_address', + }, metrics, requiredMetrics: [ 'containerOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts index 9e0153c5d6ea6b..73a10cbadb66d0 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/metrics/index.ts @@ -30,4 +30,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { cpu, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx index ddb3c0491f164e..9ed2cbe6dea084 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/container/toolbar_items.tsx @@ -4,61 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toMetricOpt, - toGroupByOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const ContainerToolbarItems = (props: ToolbarProps) => { - const options = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => - [ - 'host.name', - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ].map(toGroupByOpt), - [] - ); + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + ]; + const groupByFields = [ + 'host.name', + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', + ]; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts b/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.ts new file mode 100644 index 00000000000000..7036b2236881f8 --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/create_tsvb_model.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 { TSVBMetricModelCreator, TSVBMetricModel, TSVBSeries, InventoryMetric } from './types'; + +export const createTSVBModel = ( + id: InventoryMetric, + requires: string[], + series: TSVBSeries[], + interval = '>=300s', + dropLastBucket = true +): TSVBMetricModelCreator => (timeField, indexPattern): TSVBMetricModel => ({ + id, + requires, + drop_last_bucket: dropLastBucket, + index_pattern: indexPattern, + interval, + time_field: timeField, + type: 'timeseries', + series, +}); diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts index 08056e650a32e3..54d3267eef57a8 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; import { @@ -13,7 +14,21 @@ import { export const host: InventoryModel = { id: 'host', + displayName: i18n.translate('xpack.infra.inventoryModel.host.displayName', { + defaultMessage: 'Hosts', + }), requiredModules: ['system'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'host.name', + name: 'host.name', + ip: 'host.ip', + }, metrics, requiredMetrics: [ 'hostSystemOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts index f4c0150309dd8c..7f77f23e4fb957 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/metrics/index.ts @@ -52,4 +52,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { count, cpu, load, logRate, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx index 8e1bb0dfb48163..f8df81a33a8ec0 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/host/toolbar_items.tsx @@ -4,63 +4,32 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toGroupByOpt, - toMetricOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const HostToolbarItems = (props: ToolbarProps) => { - const metricOptions = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.load, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - InfraSnapshotMetricType.logRate, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => - [ - 'cloud.availability_zone', - 'cloud.machine.type', - 'cloud.project.id', - 'cloud.provider', - 'service.type', - ].map(toGroupByOpt), - [] - ); - + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.load, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + InfraSnapshotMetricType.logRate, + ]; + const groupByFields = [ + 'cloud.availability_zone', + 'cloud.machine.type', + 'cloud.project.id', + 'cloud.provider', + 'service.type', + ]; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/index.ts index 79aad7b2ccf6fe..d9fd8fa465b7ac 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/index.ts @@ -7,11 +7,15 @@ import { i18n } from '@kbn/i18n'; import { host } from './host'; import { pod } from './pod'; +import { awsEC2 } from './aws_ec2'; +import { awsS3 } from './aws_s3'; +import { awsRDS } from './aws_rds'; +import { awsSQS } from './aws_sqs'; import { container } from './container'; import { InventoryItemType } from './types'; export { metrics } from './metrics'; -const inventoryModels = [host, pod, container]; +export const inventoryModels = [host, pod, container, awsEC2, awsS3, awsRDS, awsSQS]; export const findInventoryModel = (type: InventoryItemType) => { const model = inventoryModels.find(m => m.id === type); @@ -24,3 +28,38 @@ export const findInventoryModel = (type: InventoryItemType) => { } return model; }; + +interface InventoryFields { + message: string[]; + host: string; + pod: string; + container: string; + timestamp: string; + tiebreaker: string; +} + +const LEGACY_TYPES = ['host', 'pod', 'container']; + +const getFieldByType = (type: InventoryItemType, fields: InventoryFields) => { + switch (type) { + case 'pod': + return fields.pod; + case 'host': + return fields.host; + case 'container': + return fields.container; + } +}; + +export const findInventoryFields = (type: InventoryItemType, fields: InventoryFields) => { + const inventoryModel = findInventoryModel(type); + if (LEGACY_TYPES.includes(type)) { + const id = getFieldByType(type, fields) || inventoryModel.fields.id; + return { + ...inventoryModel.fields, + id, + }; + } else { + return inventoryModel.fields; + } +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts b/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts index 0c593bec1af3aa..d9008753adf7bb 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/layouts.ts @@ -17,6 +17,10 @@ import { ReactNode, FunctionComponent } from 'react'; import { Layout as HostLayout } from './host/layout'; import { Layout as PodLayout } from './pod/layout'; import { Layout as ContainerLayout } from './container/layout'; +import { Layout as AwsEC2Layout } from './aws_ec2/layout'; +import { Layout as AwsS3Layout } from './aws_s3/layout'; +import { Layout as AwsRDSLayout } from './aws_rds/layout'; +import { Layout as AwsSQSLayout } from './aws_sqs/layout'; import { InventoryItemType } from './types'; import { LayoutProps } from '../../public/pages/metrics/types'; @@ -28,6 +32,10 @@ const layouts: Layouts = { host: HostLayout, pod: PodLayout, container: ContainerLayout, + awsEC2: AwsEC2Layout, + awsS3: AwsS3Layout, + awsRDS: AwsRDSLayout, + awsSQS: AwsSQSLayout, }; export const findLayout = (type: InventoryItemType) => { diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts b/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts index 78dc262b29bacd..cadc059fc5aeb2 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/metrics.ts @@ -8,6 +8,10 @@ import { metrics as hostMetrics } from './host/metrics'; import { metrics as sharedMetrics } from './shared/metrics'; import { metrics as podMetrics } from './pod/metrics'; import { metrics as containerMetrics } from './container/metrics'; +import { metrics as awsEC2Metrics } from './aws_ec2/metrics'; +import { metrics as awsS3Metrics } from './aws_s3/metrics'; +import { metrics as awsRDSMetrics } from './aws_rds/metrics'; +import { metrics as awsSQSMetrics } from './aws_sqs/metrics'; export const metrics = { tsvb: { @@ -15,5 +19,9 @@ export const metrics = { ...sharedMetrics.tsvb, ...podMetrics.tsvb, ...containerMetrics.tsvb, + ...awsEC2Metrics.tsvb, + ...awsS3Metrics.tsvb, + ...awsRDSMetrics.tsvb, + ...awsSQSMetrics.tsvb, }, }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts index 66ace03abac00b..3efc5827b4f230 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/index.ts @@ -4,13 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { metrics } from './metrics'; import { InventoryModel } from '../types'; import { nginx as nginxRequiredMetrics } from '../shared/metrics/required_metrics'; export const pod: InventoryModel = { id: 'pod', + displayName: i18n.translate('xpack.infra.inventoryModel.pod.displayName', { + defaultMessage: 'Kubernetes Pods', + }), requiredModules: ['kubernetes'], + crosslinkSupport: { + details: true, + logs: true, + apm: true, + uptime: true, + }, + fields: { + id: 'kubernetes.pod.uid', + name: 'kubernetes.pod.name', + ip: 'kubernetes.pod.ip', + }, metrics, requiredMetrics: [ 'podOverview', diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts index 2aa7ac6b496afa..b4420b5532cc6c 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/metrics/index.ts @@ -26,4 +26,5 @@ export const metrics: InventoryMetrics = { }, snapshot: { cpu, memory, rx, tx }, defaultSnapshot: 'cpu', + defaultTimeRangeInSeconds: 3600, // 1 hour }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx index cc0676fc60ae43..9ef4a889dc5891 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx +++ b/x-pack/legacy/plugins/infra/common/inventory_models/pod/toolbar_items.tsx @@ -4,54 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; -import { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; import { ToolbarProps } from '../../../public/components/inventory/toolbars/toolbar'; -import { WaffleMetricControls } from '../../../public/components/waffle/waffle_metric_controls'; -import { WaffleGroupByControls } from '../../../public/components/waffle/waffle_group_by_controls'; -import { InfraSnapshotMetricType } from '../../../public/graphql/types'; -import { - toGroupByOpt, - toMetricOpt, -} from '../../../public/components/inventory/toolbars/toolbar_wrapper'; +import { MetricsAndGroupByToolbarItems } from '../shared/compontents/metrics_and_groupby_toolbar_items'; +import { InfraSnapshotMetricType } from '../../graphql/types'; export const PodToolbarItems = (props: ToolbarProps) => { - const options = useMemo( - () => - [ - InfraSnapshotMetricType.cpu, - InfraSnapshotMetricType.memory, - InfraSnapshotMetricType.rx, - InfraSnapshotMetricType.tx, - ].map(toMetricOpt), - [] - ); - - const groupByOptions = useMemo( - () => ['kubernetes.namespace', 'kubernetes.node.name', 'service.type'].map(toGroupByOpt), - [] - ); - + const metricTypes = [ + InfraSnapshotMetricType.cpu, + InfraSnapshotMetricType.memory, + InfraSnapshotMetricType.rx, + InfraSnapshotMetricType.tx, + ]; + const groupByFields = ['kubernetes.namespace', 'kubernetes.node.name', 'service.type']; return ( - <> - - - - - - - + ); }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx b/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx new file mode 100644 index 00000000000000..c46ad5c6df952b --- /dev/null +++ b/x-pack/legacy/plugins/infra/common/inventory_models/shared/compontents/metrics_and_groupby_toolbar_items.tsx @@ -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 React, { useMemo } from 'react'; +import { EuiFlexItem } from '@elastic/eui'; +import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar'; +import { WaffleMetricControls } from '../../../../public/components/waffle/waffle_metric_controls'; +import { WaffleGroupByControls } from '../../../../public/components/waffle/waffle_group_by_controls'; +import { InfraSnapshotMetricType } from '../../../../public/graphql/types'; +import { + toGroupByOpt, + toMetricOpt, +} from '../../../../public/components/inventory/toolbars/toolbar_wrapper'; + +interface Props extends ToolbarProps { + metricTypes: InfraSnapshotMetricType[]; + groupByFields: string[]; +} + +export const MetricsAndGroupByToolbarItems = (props: Props) => { + const metricOptions = useMemo(() => props.metricTypes.map(toMetricOpt), [props.metricTypes]); + + const groupByOptions = useMemo(() => props.groupByFields.map(toGroupByOpt), [ + props.groupByFields, + ]); + + return ( + <> + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts b/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts index 6416aa08e85851..2bab5c5229c5b5 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/shared/metrics/index.ts @@ -35,4 +35,5 @@ export const metrics: InventoryMetrics = { count, }, defaultSnapshot: 'count', + defaultTimeRangeInSeconds: 3600, }; diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts b/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts index dc3c409ac497e8..05def078c7f2d1 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/toolbars.ts @@ -11,6 +11,10 @@ import { HostToolbarItems } from './host/toolbar_items'; import { ContainerToolbarItems } from './container/toolbar_items'; import { PodToolbarItems } from './pod/toolbar_items'; import { ToolbarProps } from '../../public/components/inventory/toolbars/toolbar'; +import { AwsEC2ToolbarItems } from './aws_ec2/toolbar_items'; +import { AwsS3ToolbarItems } from './aws_s3/toolbar_items'; +import { AwsRDSToolbarItems } from './aws_rds/toolbar_items'; +import { AwsSQSToolbarItems } from './aws_sqs/toolbar_items'; interface Toolbars { [type: string]: ReactNode; @@ -20,6 +24,10 @@ const toolbars: Toolbars = { host: HostToolbarItems, container: ContainerToolbarItems, pod: PodToolbarItems, + awsEC2: AwsEC2ToolbarItems, + awsS3: AwsS3ToolbarItems, + awsRDS: AwsRDSToolbarItems, + awsSQS: AwsSQSToolbarItems, }; export const findToolbar = (type: InventoryItemType) => { diff --git a/x-pack/legacy/plugins/infra/common/inventory_models/types.ts b/x-pack/legacy/plugins/infra/common/inventory_models/types.ts index 93eaf214ad23e9..e1cbdcb52ff275 100644 --- a/x-pack/legacy/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/legacy/plugins/infra/common/inventory_models/types.ts @@ -30,6 +30,7 @@ export const InventoryFormatterTypeRT = rt.keyof({ bytes: null, number: null, percent: null, + highPercision: null, }); export type InventoryFormatterType = rt.TypeOf; export type InventoryItemType = rt.TypeOf; @@ -72,6 +73,24 @@ export const InventoryMetricRT = rt.keyof({ awsNetworkPackets: null, awsDiskioBytes: null, awsDiskioOps: null, + awsEC2CpuUtilization: null, + awsEC2NetworkTraffic: null, + awsEC2DiskIOBytes: null, + awsS3TotalRequests: null, + awsS3NumberOfObjects: null, + awsS3BucketSize: null, + awsS3DownloadBytes: null, + awsS3UploadBytes: null, + awsRDSCpuTotal: null, + awsRDSConnections: null, + awsRDSQueriesExecuted: null, + awsRDSActiveTransactions: null, + awsRDSLatency: null, + awsSQSMessagesVisible: null, + awsSQSMessagesDelayed: null, + awsSQSMessagesSent: null, + awsSQSMessagesEmpty: null, + awsSQSOldestMessage: null, custom: null, }); export type InventoryMetric = rt.TypeOf; @@ -162,6 +181,8 @@ export const TSVBSeriesRT = rt.intersection([ }), ]); +export type TSVBSeries = rt.TypeOf; + export const TSVBMetricModelRT = rt.intersection([ rt.type({ id: InventoryMetricRT, @@ -176,6 +197,7 @@ export const TSVBMetricModelRT = rt.intersection([ filter: rt.string, map_field_to: rt.string, id_type: rt.keyof({ cloud: null, node: null }), + drop_last_bucket: rt.boolean, }), ]); @@ -267,6 +289,22 @@ export const SnapshotMetricTypeRT = rt.keyof({ tx: null, rx: null, logRate: null, + diskIOReadBytes: null, + diskIOWriteBytes: null, + s3TotalRequests: null, + s3NumberOfObjects: null, + s3BucketSize: null, + s3DownloadBytes: null, + s3UploadBytes: null, + rdsConnections: null, + rdsQueriesExecuted: null, + rdsActiveTransactions: null, + rdsLatency: null, + sqsMessagesVisible: null, + sqsMessagesDelayed: null, + sqsMessagesSent: null, + sqsMessagesEmpty: null, + sqsOldestMessage: null, }); export type SnapshotMetricType = rt.TypeOf; @@ -275,11 +313,25 @@ export interface InventoryMetrics { tsvb: { [name: string]: TSVBMetricModelCreator }; snapshot: { [name: string]: SnapshotModel }; defaultSnapshot: SnapshotMetricType; + /** This is used by the inventory view to calculate the appropriate amount of time for the metrics detail page. Some metris like awsS3 require multiple days where others like host only need an hour.*/ + defaultTimeRangeInSeconds: number; } export interface InventoryModel { id: string; + displayName: string; requiredModules: string[]; + fields: { + id: string; + name: string; + ip?: string; + }; + crosslinkSupport: { + details: boolean; + logs: boolean; + apm: boolean; + uptime: boolean; + }; metrics: InventoryMetrics; requiredMetrics: InventoryMetric[]; } diff --git a/x-pack/legacy/plugins/infra/index.ts b/x-pack/legacy/plugins/infra/index.ts index dbf1f4ad61de3b..5466f2572a48e8 100644 --- a/x-pack/legacy/plugins/infra/index.ts +++ b/x-pack/legacy/plugins/infra/index.ts @@ -14,7 +14,7 @@ import { getConfigSchema } from './server/kibana.index'; import { savedObjectMappings } from './server/saved_objects'; import { plugin, InfraServerPluginDeps } from './server/new_platform_index'; import { InfraSetup } from '../../../plugins/infra/server'; -import { APMPluginContract } from '../../../plugins/apm/server/plugin'; +import { APMPluginContract } from '../../../plugins/apm/server'; const APP_ID = 'infra'; const logsSampleDataLinkLabel = i18n.translate('xpack.infra.sampleDataLinkLabel', { diff --git a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx index 78255c55df124d..46b505d4fab529 100644 --- a/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx +++ b/x-pack/legacy/plugins/infra/public/components/formatted_time.tsx @@ -37,7 +37,6 @@ export const useFormattedTime = ( const dateFormat = formatMap[format]; const formattedTime = useMemo(() => getFormattedTime(time, dateFormat, fallbackFormat), [ - getFormattedTime, time, dateFormat, fallbackFormat, diff --git a/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx b/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx index 47858624fde0fd..cb48c99963d17f 100644 --- a/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx +++ b/x-pack/legacy/plugins/infra/public/components/inventory/layout.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { InfraWaffleMapOptions, InfraWaffleMapBounds } from '../../lib/lib'; import { InfraNodeType, - InfraTimerangeInput, InfraSnapshotMetricInput, InfraSnapshotGroupbyInput, } from '../../graphql/types'; @@ -23,7 +22,7 @@ export interface LayoutProps { options: InfraWaffleMapOptions; nodeType: InfraNodeType; onDrilldown: (filter: KueryFilterQuery) => void; - timeRange: InfraTimerangeInput; + currentTime: number; onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; @@ -42,7 +41,7 @@ export const Layout = (props: LayoutProps) => { props.groupBy, props.nodeType, props.sourceId, - props.timeRange + props.currentTime ); return ( <> @@ -55,7 +54,7 @@ export const Layout = (props: LayoutProps) => { loading={loading} reload={reload} onDrilldown={props.onDrilldown} - timeRange={props.timeRange} + currentTime={props.currentTime} onViewChange={props.onViewChange} view={props.view} autoBounds={props.autoBounds} diff --git a/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx b/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx index 7cb86f6e4d0eca..c4721fee4b7462 100644 --- a/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx +++ b/x-pack/legacy/plugins/infra/public/components/inventory/toolbars/toolbar_wrapper.tsx @@ -81,6 +81,54 @@ const ToolbarTranslations = { Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { defaultMessage: 'Count', }), + DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { + defaultMessage: 'Disk Reads', + }), + DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { + defaultMessage: 'Disk Writes', + }), + s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { + defaultMessage: 'Bucket Size', + }), + s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { + defaultMessage: 'Total Requests', + }), + s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { + defaultMessage: 'Number of Objects', + }), + s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { + defaultMessage: 'Downloads (Bytes)', + }), + s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { + defaultMessage: 'Uploads (Bytes)', + }), + rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { + defaultMessage: 'Connections', + }), + rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { + defaultMessage: 'Queries Executed', + }), + rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { + defaultMessage: 'Active Transactions', + }), + rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { + defaultMessage: 'Latency', + }), + sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { + defaultMessage: 'Messages Available', + }), + sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { + defaultMessage: 'Messages Delayed', + }), + sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { + defaultMessage: 'Messages Added', + }), + sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { + defaultMessage: 'Messages Returned Empty', + }), + sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { + defaultMessage: 'Oldest Message', + }), }; export const toGroupByOpt = (field: string) => ({ @@ -126,5 +174,85 @@ export const toMetricOpt = (metric: InfraSnapshotMetricType) => { text: ToolbarTranslations.Count, value: InfraSnapshotMetricType.count, }; + case InfraSnapshotMetricType.diskIOReadBytes: + return { + text: ToolbarTranslations.DiskIOReadBytes, + value: InfraSnapshotMetricType.diskIOReadBytes, + }; + case InfraSnapshotMetricType.diskIOWriteBytes: + return { + text: ToolbarTranslations.DiskIOWriteBytes, + value: InfraSnapshotMetricType.diskIOWriteBytes, + }; + case InfraSnapshotMetricType.s3BucketSize: + return { + text: ToolbarTranslations.s3BucketSize, + value: InfraSnapshotMetricType.s3BucketSize, + }; + case InfraSnapshotMetricType.s3TotalRequests: + return { + text: ToolbarTranslations.s3TotalRequests, + value: InfraSnapshotMetricType.s3TotalRequests, + }; + case InfraSnapshotMetricType.s3NumberOfObjects: + return { + text: ToolbarTranslations.s3NumberOfObjects, + value: InfraSnapshotMetricType.s3NumberOfObjects, + }; + case InfraSnapshotMetricType.s3DownloadBytes: + return { + text: ToolbarTranslations.s3DownloadBytes, + value: InfraSnapshotMetricType.s3DownloadBytes, + }; + case InfraSnapshotMetricType.s3UploadBytes: + return { + text: ToolbarTranslations.s3UploadBytes, + value: InfraSnapshotMetricType.s3UploadBytes, + }; + case InfraSnapshotMetricType.rdsConnections: + return { + text: ToolbarTranslations.rdsConnections, + value: InfraSnapshotMetricType.rdsConnections, + }; + case InfraSnapshotMetricType.rdsQueriesExecuted: + return { + text: ToolbarTranslations.rdsQueriesExecuted, + value: InfraSnapshotMetricType.rdsQueriesExecuted, + }; + case InfraSnapshotMetricType.rdsActiveTransactions: + return { + text: ToolbarTranslations.rdsActiveTransactions, + value: InfraSnapshotMetricType.rdsActiveTransactions, + }; + case InfraSnapshotMetricType.rdsLatency: + return { + text: ToolbarTranslations.rdsLatency, + value: InfraSnapshotMetricType.rdsLatency, + }; + case InfraSnapshotMetricType.sqsMessagesVisible: + return { + text: ToolbarTranslations.sqsMessagesVisible, + value: InfraSnapshotMetricType.sqsMessagesVisible, + }; + case InfraSnapshotMetricType.sqsMessagesDelayed: + return { + text: ToolbarTranslations.sqsMessagesDelayed, + value: InfraSnapshotMetricType.sqsMessagesDelayed, + }; + case InfraSnapshotMetricType.sqsMessagesSent: + return { + text: ToolbarTranslations.sqsMessagesSent, + value: InfraSnapshotMetricType.sqsMessagesSent, + }; + case InfraSnapshotMetricType.sqsMessagesEmpty: + return { + text: ToolbarTranslations.sqsMessagesEmpty, + value: InfraSnapshotMetricType.sqsMessagesEmpty, + }; + case InfraSnapshotMetricType.sqsOldestMessage: + return { + text: ToolbarTranslations.sqsOldestMessage, + value: InfraSnapshotMetricType.sqsOldestMessage, + }; } }; diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx index 92c6ddd1936090..d018b3a0f38ff0 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_entry_flyout/log_entry_actions_menu.tsx @@ -51,7 +51,7 @@ export const LogEntryActionsMenu: React.FunctionComponent<{ /> , ], - [uptimeLink] + [apmLink, uptimeLink] ); const hasMenuItems = useMemo(() => menuItems.length > 0, [menuItems]); diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx index 24a5e8bacb4f93..d13ccde7466cd5 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_highlights_menu.tsx @@ -16,7 +16,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; import { useVisibilityState } from '../../utils/use_visibility_state'; @@ -47,8 +47,25 @@ export const LogHighlightsMenu: React.FC = ({ } = useVisibilityState(false); // Input field state - const [highlightTerm, setHighlightTerm] = useState(''); + const [highlightTerm, _setHighlightTerm] = useState(''); + const debouncedOnChange = useMemo(() => debounce(onChange, 275), [onChange]); + const setHighlightTerm = useCallback( + valueOrUpdater => + _setHighlightTerm(previousHighlightTerm => { + const newHighlightTerm = + typeof valueOrUpdater === 'function' + ? valueOrUpdater(previousHighlightTerm) + : valueOrUpdater; + + if (newHighlightTerm !== previousHighlightTerm) { + debouncedOnChange([newHighlightTerm]); + } + + return newHighlightTerm; + }), + [debouncedOnChange] + ); const changeHighlightTerm = useCallback( e => { const value = e.target.value; @@ -57,9 +74,6 @@ export const LogHighlightsMenu: React.FC = ({ [setHighlightTerm] ); const clearHighlightTerm = useCallback(() => setHighlightTerm(''), [setHighlightTerm]); - useEffect(() => { - debouncedOnChange([highlightTerm]); - }, [highlightTerm]); const button = ( diff --git a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 1d40c88f5d1d01..e95ac6aa7923b1 100644 --- a/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/legacy/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -63,7 +63,7 @@ export const useMeasuredCharacterDimensions = (scale: TextScale) => { X ), - [scale] + [measureElement, scale] ); return { diff --git a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx index d59e709d9a19a3..42df7c6915a0df 100644 --- a/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx +++ b/x-pack/legacy/plugins/infra/public/components/metrics_explorer/metrics.tsx @@ -7,7 +7,7 @@ import { EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useState } from 'react'; import { FieldType } from 'ui/index_patterns'; import { colorTransformer, MetricsExplorerColor } from '../../../common/color_palette'; import { @@ -31,24 +31,19 @@ interface SelectedOption { export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = false }: Props) => { const colors = Object.keys(MetricsExplorerColor) as MetricsExplorerColor[]; - const [inputRef, setInputRef] = useState(null); - const [focusOnce, setFocusState] = useState(false); + const [shouldFocus, setShouldFocus] = useState(autoFocus); - useEffect(() => { - if (inputRef && autoFocus && !focusOnce) { - inputRef.focus(); - setFocusState(true); - } - }, [inputRef]); + // the EuiCombobox forwards the ref to an input element + const autoFocusInputElement = useCallback( + (inputElement: HTMLInputElement | null) => { + if (inputElement && shouldFocus) { + inputElement.focus(); + setShouldFocus(false); + } + }, + [shouldFocus] + ); - // I tried to use useRef originally but the EUIComboBox component's type definition - // would only accept an actual input element or a callback function (with the same type). - // This effectivly does the same thing but is compatible with EuiComboBox. - const handleInputRef = (ref: HTMLInputElement) => { - if (ref) { - setInputRef(ref); - } - }; const handleChange = useCallback( selectedOptions => { onChange( @@ -59,7 +54,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = })) ); }, - [options, onChange] + [onChange, options.aggregation, colors] ); const comboOptions = fields @@ -86,7 +81,7 @@ export const MetricsExplorerMetrics = ({ options, onChange, fields, autoFocus = selectedOptions={selectedOptions} onChange={handleChange} isClearable={true} - inputRef={handleInputRef} + inputRef={autoFocusInputElement} /> ); }; diff --git a/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx b/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx index acddbee8db267f..edf1b228b278a5 100644 --- a/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx +++ b/x-pack/legacy/plugins/infra/public/components/nodes_overview/index.tsx @@ -11,12 +11,7 @@ import { get, max, min } from 'lodash'; import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { - InfraSnapshotMetricType, - InfraSnapshotNode, - InfraNodeType, - InfraTimerangeInput, -} from '../../graphql/types'; +import { InfraSnapshotMetricType, InfraSnapshotNode, InfraNodeType } from '../../graphql/types'; import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; import { KueryFilterQuery } from '../../store/local/waffle_filter'; import { createFormatter } from '../../utils/formatters'; @@ -34,7 +29,7 @@ interface Props { loading: boolean; reload: () => void; onDrilldown: (filter: KueryFilterQuery) => void; - timeRange: InfraTimerangeInput; + currentTime: number; onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; @@ -67,6 +62,38 @@ const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.abbreviatedNumber, template: '{{value}}/s', }, + [InfraSnapshotMetricType.diskIOReadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + [InfraSnapshotMetricType.diskIOWriteBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + [InfraSnapshotMetricType.s3BucketSize]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3TotalRequests]: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3NumberOfObjects]: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3UploadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.s3DownloadBytes]: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + [InfraSnapshotMetricType.sqsOldestMessage]: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, }; const calculateBoundsFromNodes = (nodes: InfraSnapshotNode[]): InfraWaffleMapBounds => { @@ -92,8 +119,8 @@ export const NodesOverview = class extends React.Component { nodeType, reload, view, + currentTime, options, - timeRange, } = this.props; if (loading) { return ( @@ -152,7 +179,7 @@ export const NodesOverview = class extends React.Component { nodes={nodes} options={options} formatter={this.formatter} - timeRange={timeRange} + currentTime={currentTime} onFilter={this.handleDrilldown} /> @@ -163,7 +190,7 @@ export const NodesOverview = class extends React.Component { nodes={nodes} options={options} formatter={this.formatter} - timeRange={timeRange} + currentTime={currentTime} onFilter={this.handleDrilldown} bounds={bounds} dataBounds={dataBounds} diff --git a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx index 5b201d2be43333..b4abf962bd8929 100644 --- a/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx +++ b/x-pack/legacy/plugins/infra/public/components/nodes_overview/table.tsx @@ -10,12 +10,7 @@ import { i18n } from '@kbn/i18n'; import { last } from 'lodash'; import React from 'react'; import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap'; -import { - InfraSnapshotNode, - InfraSnapshotNodePath, - InfraTimerangeInput, - InfraNodeType, -} from '../../graphql/types'; +import { InfraSnapshotNode, InfraSnapshotNodePath, InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { fieldToName } from '../waffle/lib/field_to_display_name'; import { NodeContextMenu } from '../waffle/node_context_menu'; @@ -25,7 +20,7 @@ interface Props { nodeType: InfraNodeType; options: InfraWaffleMapOptions; formatter: (subject: string | number) => string; - timeRange: InfraTimerangeInput; + currentTime: number; onFilter: (filter: string) => void; } @@ -49,7 +44,7 @@ const getGroupPaths = (path: InfraSnapshotNodePath[]) => { export const TableView = class extends React.PureComponent { public readonly state: State = initialState; public render() { - const { nodes, options, formatter, timeRange, nodeType } = this.props; + const { nodes, options, formatter, currentTime, nodeType } = this.props; const columns = [ { field: 'name', @@ -68,7 +63,7 @@ export const TableView = class extends React.PureComponent { node={item.node} nodeType={nodeType} closePopover={this.closePopoverFor(uniqueID)} - timeRange={timeRange} + currentTime={currentTime} isPopoverOpen={this.state.isPopoverOpen.includes(uniqueID)} options={options} popoverPosition="rightCenter" diff --git a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx index 8df479f36e2f99..9b8907a1ff9e17 100644 --- a/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/legacy/plugins/infra/public/components/saved_views/create_modal.tsx @@ -36,7 +36,7 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { const saveView = useCallback(() => { save(viewName, includeTime); - }, [viewName, includeTime]); + }, [includeTime, save, viewName]); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx index 9b83f62e7856b9..fc8407c5298e60 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/add_log_column_popover.tsx @@ -94,7 +94,7 @@ export const AddLogColumnButtonAndPopover: React.FunctionComponent<{ addLogColumn(selectedOption.columnConfiguration); }, - [addLogColumn, availableColumnOptions] + [addLogColumn, availableColumnOptions, closePopover] ); return ( diff --git a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index 771285e8ccee4c..5f3d1a63e72eb3 100644 --- a/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/legacy/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -50,10 +50,12 @@ export const FieldsConfigurationPanel = ({ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ +

+ +

} description={ { indicesConfigurationFormState.resetForm(); logColumnsConfigurationFormState.resetForm(); - }, [indicesConfigurationFormState.resetForm, logColumnsConfigurationFormState.formState]); + }, [indicesConfigurationFormState, logColumnsConfigurationFormState]); const isFormDirty = useMemo( () => indicesConfigurationFormState.isFormDirty || logColumnsConfigurationFormState.isFormDirty, diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx index 3f456c3c8d4068..7a229fbbe02ec6 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_groups.tsx @@ -7,7 +7,7 @@ import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfGroups, @@ -23,7 +23,7 @@ interface Props { formatter: (val: number) => string; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const GroupOfGroups: React.FC = props => { @@ -41,7 +41,7 @@ export const GroupOfGroups: React.FC = props => { formatter={props.formatter} bounds={props.bounds} nodeType={props.nodeType} - timeRange={props.timeRange} + currentTime={props.currentTime} /> ))} diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx index bc7d31a3014965..c40c68cbdbf280 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/group_of_nodes.tsx @@ -7,7 +7,7 @@ import React from 'react'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapGroupOfNodes, @@ -24,7 +24,7 @@ interface Props { isChild: boolean; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const GroupOfNodes: React.FC = ({ @@ -35,7 +35,7 @@ export const GroupOfNodes: React.FC = ({ isChild = false, bounds, nodeType, - timeRange, + currentTime, }) => { const width = group.width > 200 ? group.width : 200; return ( @@ -51,7 +51,7 @@ export const GroupOfNodes: React.FC = ({ formatter={formatter} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ))} diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts index b34b2801a50f1c..7160c8eaa8dde0 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts +++ b/x-pack/legacy/plugins/infra/public/components/waffle/lib/field_to_display_name.ts @@ -10,6 +10,14 @@ interface Lookup { [id: string]: string; } +const availabilityZoneName = i18n.translate('xpack.infra.groupByDisplayNames.availabilityZone', { + defaultMessage: 'Availability zone', +}); + +const machineTypeName = i18n.translate('xpack.infra.groupByDisplayNames.machineType', { + defaultMessage: 'Machine type', +}); + export const fieldToName = (field: string) => { const LOOKUP: Lookup = { 'kubernetes.namespace': i18n.translate('xpack.infra.groupByDisplayNames.kubernetesNamespace', { @@ -21,12 +29,8 @@ export const fieldToName = (field: string) => { 'host.name': i18n.translate('xpack.infra.groupByDisplayNames.hostName', { defaultMessage: 'Host', }), - 'cloud.availability_zone': i18n.translate('xpack.infra.groupByDisplayNames.availabilityZone', { - defaultMessage: 'Availability zone', - }), - 'cloud.machine.type': i18n.translate('xpack.infra.groupByDisplayNames.machineType', { - defaultMessage: 'Machine type', - }), + 'cloud.availability_zone': availabilityZoneName, + 'cloud.machine.type': machineTypeName, 'cloud.project.id': i18n.translate('xpack.infra.groupByDisplayNames.projectID', { defaultMessage: 'Project ID', }), @@ -36,6 +40,32 @@ export const fieldToName = (field: string) => { 'service.type': i18n.translate('xpack.infra.groupByDisplayNames.serviceType', { defaultMessage: 'Service type', }), + 'aws.cloud.availability_zone': availabilityZoneName, + 'aws.cloud.machine.type': machineTypeName, + 'aws.tags': i18n.translate('xpack.infra.groupByDisplayNames.tags', { + defaultMessage: 'Tags', + }), + 'aws.ec2.instance.image.id': i18n.translate('xpack.infra.groupByDisplayNames.image', { + defaultMessage: 'Image', + }), + 'aws.ec2.instance.state.name': i18n.translate('xpack.infra.groupByDisplayNames.state.name', { + defaultMessage: 'State', + }), + 'cloud.region': i18n.translate('xpack.infra.groupByDisplayNames.cloud.region', { + defaultMessage: 'Region', + }), + 'aws.rds.db_instance.class': i18n.translate( + 'xpack.infra.groupByDisplayNames.rds.db_instance.class', + { + defaultMessage: 'Instance Class', + } + ), + 'aws.rds.db_instance.status': i18n.translate( + 'xpack.infra.groupByDisplayNames.rds.db_instance.status', + { + defaultMessage: 'Status', + } + ), }; return LOOKUP[field] || field; }; diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx index ed7db4fe3dfe16..6c0209a60f1cd5 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/map.tsx @@ -11,7 +11,7 @@ import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes, } from '../../containers/waffle/type_guards'; -import { InfraSnapshotNode, InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; +import { InfraSnapshotNode, InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib'; import { AutoSizer } from '../auto_sizer'; import { GroupOfGroups } from './group_of_groups'; @@ -24,7 +24,7 @@ interface Props { nodeType: InfraNodeType; options: InfraWaffleMapOptions; formatter: (subject: string | number) => string; - timeRange: InfraTimerangeInput; + currentTime: number; onFilter: (filter: string) => void; bounds: InfraWaffleMapBounds; dataBounds: InfraWaffleMapBounds; @@ -33,7 +33,7 @@ interface Props { export const Map: React.FC = ({ nodes, options, - timeRange, + currentTime, onFilter, formatter, bounds, @@ -59,7 +59,7 @@ export const Map: React.FC = ({ formatter={formatter} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ); } @@ -74,7 +74,7 @@ export const Map: React.FC = ({ isChild={false} bounds={bounds} nodeType={nodeType} - timeRange={timeRange} + currentTime={currentTime} /> ); } diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx index 8f09a3fdca9cf6..f0770064c3cf99 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/node.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; import { darken, readableColor } from 'polished'; import React from 'react'; @@ -12,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { ConditionalToolTip } from './conditional_tooltip'; import euiStyled from '../../../../../common/eui_styled_components'; -import { InfraTimerangeInput, InfraNodeType } from '../../graphql/types'; +import { InfraNodeType } from '../../graphql/types'; import { InfraWaffleMapBounds, InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { colorFromValue } from './lib/color_from_value'; import { NodeContextMenu } from './node_context_menu'; @@ -30,13 +29,13 @@ interface Props { formatter: (val: number) => string; bounds: InfraWaffleMapBounds; nodeType: InfraNodeType; - timeRange: InfraTimerangeInput; + currentTime: number; } export const Node = class extends React.PureComponent { public readonly state: State = initialState; public render() { - const { nodeType, node, options, squareSize, bounds, formatter, timeRange } = this.props; + const { nodeType, node, options, squareSize, bounds, formatter, currentTime } = this.props; const { isPopoverOpen } = this.state; const { metric } = node; const valueMode = squareSize > 70; @@ -44,12 +43,6 @@ export const Node = class extends React.PureComponent { const rawValue = (metric && metric.value) || 0; const color = colorFromValue(options.legend, rawValue, bounds); const value = formatter(rawValue); - const newTimerange = { - ...timeRange, - from: moment(timeRange.to) - .subtract(1, 'hour') - .valueOf(), - }; const nodeAriaLabel = i18n.translate('xpack.infra.node.ariaLabel', { defaultMessage: '{nodeName}, click to open menu', values: { nodeName: node.name }, @@ -61,7 +54,7 @@ export const Node = class extends React.PureComponent { isPopoverOpen={isPopoverOpen} closePopover={this.closePopover} options={options} - timeRange={newTimerange} + currentTime={currentTime} popoverPosition="downCenter" > { + const inventoryModel = findInventoryModel(nodeType); // Due to the changing nature of the fields between APM and this UI, // We need to have some exceptions until 7.0 & ECS is finalized. Reference // #26620 for the details for these fields. // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const APM_FIELDS = { - [InfraNodeType.host]: 'host.hostname', - [InfraNodeType.container]: 'container.id', - [InfraNodeType.pod]: 'kubernetes.pod.uid', - }; + const apmField = nodeType === InfraNodeType.host ? 'host.hostname' : inventoryModel.fields.id; const nodeLogsMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewLogsName', { @@ -62,11 +59,12 @@ export const NodeContextMenu = injectUICapabilities( href: getNodeLogsUrl({ nodeType, nodeId: node.id, - time: timeRange.to, + time: currentTime, }), 'data-test-subj': 'viewLogsContextMenuItem', }; + const nodeDetailFrom = currentTime - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; const nodeDetailMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewMetricsName', { defaultMessage: 'View metrics', @@ -74,45 +72,47 @@ export const NodeContextMenu = injectUICapabilities( href: getNodeDetailUrl({ nodeType, nodeId: node.id, - from: timeRange.from, - to: timeRange.to, + from: nodeDetailFrom, + to: currentTime, }), }; const apmTracesMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewAPMTraces', { - defaultMessage: 'View {nodeType} APM traces', - values: { nodeType }, + defaultMessage: 'View APM traces', }), - href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}:"${node.id}"`, + href: `../app/apm#/traces?_g=()&kuery=${apmField}:"${node.id}"`, 'data-test-subj': 'viewApmTracesContextMenuItem', }; const uptimeMenuItem = { name: i18n.translate('xpack.infra.nodeContextMenu.viewUptimeLink', { - defaultMessage: 'View {nodeType} in Uptime', - values: { nodeType }, + defaultMessage: 'View in Uptime', }), href: createUptimeLink(options, nodeType, node), }; - const showLogsLink = node.id && uiCapabilities.logs.show; - const showAPMTraceLink = uiCapabilities.apm && uiCapabilities.apm.show; + const showDetail = inventoryModel.crosslinkSupport.details; + const showLogsLink = + inventoryModel.crosslinkSupport.logs && node.id && uiCapabilities.logs.show; + const showAPMTraceLink = + inventoryModel.crosslinkSupport.apm && uiCapabilities.apm && uiCapabilities.apm.show; const showUptimeLink = - [InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip; + inventoryModel.crosslinkSupport.uptime && + ([InfraNodeType.pod, InfraNodeType.container].includes(nodeType) || node.ip); - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: '', - items: [ - ...(showLogsLink ? [nodeLogsMenuItem] : []), - nodeDetailMenuItem, - ...(showAPMTraceLink ? [apmTracesMenuItem] : []), - ...(showUptimeLink ? [uptimeMenuItem] : []), - ], - }, + const items = [ + ...(showLogsLink ? [nodeLogsMenuItem] : []), + ...(showDetail ? [nodeDetailMenuItem] : []), + ...(showAPMTraceLink ? [apmTracesMenuItem] : []), + ...(showUptimeLink ? [uptimeMenuItem] : []), ]; + const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, title: '', items }]; + + // If there is nothing to show then we need to return the child as is + if (items.length === 0) { + return <>{children}; + } return ( void; changeGroupBy: (groupBy: InfraSnapshotGroupbyInput[]) => void; changeMetric: (metric: InfraSnapshotMetricInput) => void; } -export const WaffleInventorySwitcher = (props: Props) => { +const getDisplayNameForType = (type: InventoryItemType) => { + const inventoryModel = findInventoryModel(type); + return inventoryModel.displayName; +}; + +export const WaffleInventorySwitcher: React.FC = ({ + changeNodeType, + changeGroupBy, + changeMetric, + nodeType, +}) => { const [isOpen, setIsOpen] = useState(false); const closePopover = useCallback(() => setIsOpen(false), []); const openPopover = useCallback(() => setIsOpen(true), []); const goToNodeType = useCallback( - (nodeType: InfraNodeType) => { + (targetNodeType: InfraNodeType) => { closePopover(); - props.changeNodeType(nodeType); - props.changeGroupBy([]); - const inventoryModel = findInventoryModel(nodeType); - props.changeMetric({ + changeNodeType(targetNodeType); + changeGroupBy([]); + const inventoryModel = findInventoryModel(targetNodeType); + changeMetric({ type: inventoryModel.metrics.defaultSnapshot as InfraSnapshotMetricType, }); }, - [props.changeGroupBy, props.changeNodeType, props.changeMetric] + [closePopover, changeNodeType, changeGroupBy, changeMetric] ); const goToHost = useCallback(() => goToNodeType('host' as InfraNodeType), [goToNodeType]); const goToK8 = useCallback(() => goToNodeType('pod' as InfraNodeType), [goToNodeType]); const goToDocker = useCallback(() => goToNodeType('container' as InfraNodeType), [goToNodeType]); + const goToAwsEC2 = useCallback(() => goToNodeType('awsEC2' as InfraNodeType), [goToNodeType]); + const goToAwsS3 = useCallback(() => goToNodeType('awsS3' as InfraNodeType), [goToNodeType]); + const goToAwsRDS = useCallback(() => goToNodeType('awsRDS' as InfraNodeType), [goToNodeType]); + const goToAwsSQS = useCallback(() => goToNodeType('awsSQS' as InfraNodeType), [goToNodeType]); const panels = useMemo( - () => [ - { - id: 0, - items: [ - { - name: i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { - defaultMessage: 'Hosts', - }), - icon: 'host', - onClick: goToHost, - }, - { - name: 'Kubernetes', - icon: 'kubernetes', - onClick: goToK8, - }, - { - name: 'Docker', - icon: 'docker', - onClick: goToDocker, - }, - ], - }, - ], - [] + () => + [ + { + id: 'firstPanel', + items: [ + { + name: getDisplayNameForType('host'), + onClick: goToHost, + }, + { + name: getDisplayNameForType('pod'), + onClick: goToK8, + }, + { + name: getDisplayNameForType('container'), + onClick: goToDocker, + }, + { + name: 'AWS', + panel: 'awsPanel', + }, + ], + }, + { + id: 'awsPanel', + title: 'AWS', + items: [ + { + name: getDisplayNameForType('awsEC2'), + onClick: goToAwsEC2, + }, + { + name: getDisplayNameForType('awsS3'), + onClick: goToAwsS3, + }, + { + name: getDisplayNameForType('awsRDS'), + onClick: goToAwsRDS, + }, + { + name: getDisplayNameForType('awsSQS'), + onClick: goToAwsSQS, + }, + ], + }, + ] as EuiContextMenuPanelDescriptor[], + [goToAwsEC2, goToAwsRDS, goToAwsS3, goToAwsSQS, goToDocker, goToHost, goToK8] ); + const selectedText = useMemo(() => { - switch (props.nodeType) { - case InfraNodeType.host: - return i18n.translate('xpack.infra.waffle.nodeTypeSwitcher.hostsLabel', { - defaultMessage: 'Hosts', - }); - case InfraNodeType.pod: - return 'Kubernetes'; - case InfraNodeType.container: - return 'Docker'; - } - }, [props.nodeType]); + return getDisplayNameForType(nodeType); + }, [nodeType]); return ( @@ -102,7 +136,7 @@ export const WaffleInventorySwitcher = (props: Props) => { withTitle anchorPosition="downLeft" > - + ); diff --git a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx index b0ea6f13f2bb61..d5ae6fcf7f7a2b 100644 --- a/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx +++ b/x-pack/legacy/plugins/infra/public/components/waffle/waffle_metric_controls.tsx @@ -44,7 +44,7 @@ export const WaffleMetricControls = class extends React.PureComponent o.value === metric.type); if (!currentLabel) { - return 'null'; + return null; } const panels: EuiContextMenuPanelDescriptor[] = [ { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx index 35a3ac737ada3c..bb01043b0db6e9 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_capabilities.tsx @@ -46,7 +46,7 @@ export const useLogAnalysisCapabilities = () => { useEffect(() => { fetchMlCapabilities(); - }, []); + }, [fetchMlCapabilities]); const isLoading = useMemo(() => fetchMlCapabilitiesRequest.state === 'pending', [ fetchMlCapabilitiesRequest.state, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 189b58d7923f84..d7d0ecb6f2c8df 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -125,23 +125,23 @@ export const useLogAnalysisModule = ({ dispatchModuleStatus({ type: 'failedSetup' }); }); }, - [cleanUpModule, setUpModule] + [cleanUpModule, dispatchModuleStatus, setUpModule] ); const viewSetupForReconfiguration = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobConfigurationUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewSetupForUpdate = useCallback(() => { dispatchModuleStatus({ type: 'requestedJobDefinitionUpdate' }); - }, []); + }, [dispatchModuleStatus]); const viewResults = useCallback(() => { dispatchModuleStatus({ type: 'viewedResults' }); - }, []); + }, [dispatchModuleStatus]); const jobIds = useMemo(() => moduleDescriptor.getJobIds(spaceId, sourceId), [ - moduleDescriptor.getJobIds, + moduleDescriptor, spaceId, sourceId, ]); diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx index 275c0194be3b2c..74dbb3c7a8062c 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.tsx @@ -140,7 +140,7 @@ export const useAnalysisSetupState = ({ ? [...errors, ...index.errors] : errors; }, []); - }, [selectedIndexNames, validatedIndices, validateIndicesRequest.state]); + }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); return { cleanupAndSetup, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx index 6ead866fb960a0..2b19958a9b1a11 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_entry_highlights.tsx @@ -78,7 +78,7 @@ export const useLogEntryHighlights = ( } else { setLogEntryHighlights([]); } - }, [highlightTerms, startKey, endKey, filterQuery, sourceVersion]); + }, [endKey, filterQuery, highlightTerms, loadLogEntryHighlights, sourceVersion, startKey]); const logEntryHighlightsById = useMemo( () => diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts index 34c66afda010ee..874c70e016496a 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/log_summary_highlights.ts @@ -74,7 +74,15 @@ export const useLogSummaryHighlights = ( } else { setLogSummaryHighlights([]); } - }, [highlightTerms, start, end, bucketSize, filterQuery, sourceVersion]); + }, [ + bucketSize, + debouncedLoadSummaryHighlights, + end, + filterQuery, + highlightTerms, + sourceVersion, + start, + ]); return { logSummaryHighlights, diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx index 95ead50119eb4d..62a43a5412825b 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/next_and_previous.tsx @@ -53,7 +53,7 @@ export const useNextAndPrevious = ({ const initialTimeKey = getUniqueLogEntryKey(entries[initialIndex]); setCurrentTimeKey(initialTimeKey); } - }, [currentTimeKey, entries, setCurrentTimeKey]); + }, [currentTimeKey, entries, setCurrentTimeKey, visibleMidpoint]); const indexOfCurrentTimeKey = useMemo(() => { if (currentTimeKey && entries.length > 0) { diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx index 2b60c6edd97aa0..9ea8987d4f3269 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx +++ b/x-pack/legacy/plugins/infra/public/containers/logs/log_highlights/redux_bridges.tsx @@ -25,11 +25,11 @@ export const LogHighlightsPositionBridge = withLogPosition( const { setJumpToTarget, setVisibleMidpoint } = useContext(LogHighlightsState.Context); useEffect(() => { setVisibleMidpoint(visibleMidpoint); - }, [visibleMidpoint]); + }, [setVisibleMidpoint, visibleMidpoint]); useEffect(() => { setJumpToTarget(() => jumpToTargetPosition); - }, [jumpToTargetPosition]); + }, [jumpToTargetPosition, setJumpToTarget]); return null; } @@ -41,7 +41,7 @@ export const LogHighlightsFilterQueryBridge = withLogFilter( useEffect(() => { setFilterQuery(serializedFilterQuery); - }, [serializedFilterQuery]); + }, [serializedFilterQuery, setFilterQuery]); return null; } diff --git a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts index da468b4391e4e1..9b20676486af29 100644 --- a/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts +++ b/x-pack/legacy/plugins/infra/public/containers/logs/with_stream_items.ts @@ -35,7 +35,7 @@ export const WithStreamItems: React.FunctionComponent<{ createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || []) ), - [logEntries.entries, logEntryHighlightsById] + [isAutoReloading, logEntries.entries, logEntries.isReloading, logEntryHighlightsById] ); return children({ diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts index 1418d6aef67ac1..c2a599ea1ae781 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_data.ts @@ -96,6 +96,9 @@ export function useMetricsExplorerData( } setLoading(false); })(); + + // TODO: fix this dependency list while preserving the semantics + // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, source, timerange, signal, afterKey]); return { error, loading, data }; } diff --git a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts index 278f3e0a9c17d8..de7a8d5805ecc6 100644 --- a/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts +++ b/x-pack/legacy/plugins/infra/public/containers/metrics_explorer/use_metrics_explorer_options.ts @@ -102,7 +102,7 @@ function useStateWithLocalStorage( const [state, setState] = useState(parseJsonOrDefault(storageState, defaultState)); useEffect(() => { localStorage.setItem(key, JSON.stringify(state)); - }, [state]); + }, [key, state]); return [state, setState]; } diff --git a/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts b/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts index 4c8d41afd36b57..63b91bd97776a2 100644 --- a/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts +++ b/x-pack/legacy/plugins/infra/public/containers/waffle/use_snaphot.ts @@ -12,7 +12,6 @@ import { InfraNodeType, InfraSnapshotMetricInput, InfraSnapshotGroupbyInput, - InfraTimerangeInput, } from '../../graphql/types'; import { throwErrors, createPlainError } from '../../../common/runtime_types'; import { useHTTPRequest } from '../../hooks/use_http_request'; @@ -27,7 +26,7 @@ export function useSnapshot( groupBy: InfraSnapshotGroupbyInput[], nodeType: InfraNodeType, sourceId: string, - timerange: InfraTimerangeInput + currentTime: number ) { const decodeResponse = (response: any) => { return pipe( @@ -36,6 +35,12 @@ export function useSnapshot( ); }; + const timerange = { + interval: '1m', + to: currentTime, + from: currentTime - 360 * 1000, + }; + const { error, loading, response, makeRequest } = useHTTPRequest( '/api/metrics/snapshot', 'POST', diff --git a/x-pack/legacy/plugins/infra/public/graphql/introspection.json b/x-pack/legacy/plugins/infra/public/graphql/introspection.json index 64792d606f446a..ae8d3dcdd5becf 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/infra/public/graphql/introspection.json @@ -2129,7 +2129,8 @@ "isDeprecated": false, "deprecationReason": null }, - { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null } + { "name": "host", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "awsEC2", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -2191,7 +2192,9 @@ { "name": "memory", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "tx", "description": "", "isDeprecated": false, "deprecationReason": null }, { "name": "rx", "description": "", "isDeprecated": false, "deprecationReason": null }, - { "name": "logRate", "description": "", "isDeprecated": false, "deprecationReason": null } + { "name": "logRate", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "diskIOReadBytes", "description": "", "isDeprecated": false, "deprecationReason": null }, + { "name": "diskIOWriteBytes", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null }, @@ -2585,6 +2588,24 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "awsEC2CpuUtilization", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awsEC2NetworkTraffic", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awsEC2DiskIOBytes", + "description": "", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null } ], "possibleTypes": null diff --git a/x-pack/legacy/plugins/infra/public/graphql/types.ts b/x-pack/legacy/plugins/infra/public/graphql/types.ts index 8d0e75523a8ed8..3715f02bb252ef 100644 --- a/x-pack/legacy/plugins/infra/public/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/public/graphql/types.ts @@ -554,6 +554,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS', } export enum InfraSnapshotMetricType { @@ -564,6 +568,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsMessagesVisible', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -604,6 +624,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx index 3a40c4fa6bf0c4..934022d6e6bd04 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_bulk_get_saved_object.tsx @@ -7,8 +7,7 @@ import { useState, useCallback } from 'react'; import { npStart } from 'ui/new_platform'; -import { SavedObjectsBatchResponse } from 'src/core/public'; -import { SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectAttributes, SavedObjectsBatchResponse } from 'src/core/public'; export const useBulkGetSavedObject = (type: string) => { const [data, setData] = useState | null>(null); diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx index 80811a6d6c7bf3..f03a198355bb8a 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_create_saved_object.tsx @@ -7,8 +7,11 @@ import { useState, useCallback } from 'react'; import { npStart } from 'ui/new_platform'; -import { SavedObjectsCreateOptions, SimpleSavedObject } from 'src/core/public'; -import { SavedObjectAttributes } from 'src/core/server'; +import { + SavedObjectAttributes, + SavedObjectsCreateOptions, + SimpleSavedObject, +} from 'src/core/public'; export const useCreateSavedObject = (type: string) => { const [data, setData] = useState | null>(null); diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx index 949a2344418e9b..2487d830266b17 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_find_saved_object.tsx @@ -7,8 +7,7 @@ import { useState, useCallback } from 'react'; import { npStart } from 'ui/new_platform'; -import { SavedObjectsBatchResponse } from 'src/core/public'; -import { SavedObjectAttributes } from 'src/core/server'; +import { SavedObjectAttributes, SavedObjectsBatchResponse } from 'src/core/public'; export const useFindSavedObject = (type: string) => { const [data, setData] = useState | null>(null); diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts index 8db0ed28d9b21a..4b12b6c51ea0eb 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts +++ b/x-pack/legacy/plugins/infra/public/hooks/use_saved_view.ts @@ -26,29 +26,32 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s >(viewType); const { create, error: errorOnCreate, createdId } = useCreateSavedObject(viewType); const { deleteObject, deletedId } = useDeleteSavedObject(viewType); - const deleteView = useCallback((id: string) => deleteObject(id), []); + const deleteView = useCallback((id: string) => deleteObject(id), [deleteObject]); const [createError, setCreateError] = useState(null); - useEffect(() => setCreateError(createError), [errorOnCreate, setCreateError]); + useEffect(() => setCreateError(errorOnCreate), [errorOnCreate]); - const saveView = useCallback((d: { [p: string]: any }) => { - const doSave = async () => { - const exists = await hasView(d.name); - if (exists) { - setCreateError( - i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { - defaultMessage: `A view with that name already exists.`, - }) - ); - return; - } - create(d); - }; - setCreateError(null); - doSave(); - }, []); + const saveView = useCallback( + (d: { [p: string]: any }) => { + const doSave = async () => { + const exists = await hasView(d.name); + if (exists) { + setCreateError( + i18n.translate('xpack.infra.savedView.errorOnCreate.duplicateViewName', { + defaultMessage: `A view with that name already exists.`, + }) + ); + return; + } + create(d); + }; + setCreateError(null); + doSave(); + }, + [create, hasView] + ); - const savedObjects = data ? data.savedObjects : []; + const savedObjects = useMemo(() => (data ? data.savedObjects : []), [data]); const views = useMemo(() => { const items: Array> = [ { @@ -61,19 +64,17 @@ export const useSavedView = (defaultViewState: ViewState, viewType: s }, ]; - if (data) { - data.savedObjects.forEach( - o => - o.type === viewType && - items.push({ - ...o.attributes, - id: o.id, - }) - ); - } + savedObjects.forEach( + o => + o.type === viewType && + items.push({ + ...o.attributes, + id: o.id, + }) + ); return items; - }, [savedObjects, defaultViewState]); + }, [defaultViewState, savedObjects, viewType]); return { views, diff --git a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx index 379b3af3f10637..c5945ab808202c 100644 --- a/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx +++ b/x-pack/legacy/plugins/infra/public/hooks/use_track_metric.tsx @@ -57,6 +57,9 @@ export function useTrackMetric( const trackUiMetric = getTrackerForApp(app); const id = setTimeout(() => trackUiMetric(metricType, decoratedMetric), Math.max(delay, 0)); return () => clearTimeout(id); + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, effectDependencies); } diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx index fe48fcc62f77d3..9efbbe790abc14 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/index.tsx @@ -24,6 +24,7 @@ import { MetricsExplorerPage } from './metrics_explorer'; import { SnapshotPage } from './snapshot'; import { SettingsPage } from '../shared/settings'; import { AppNavigation } from '../../components/navigation/app_navigation'; +import { SourceLoadingPage } from '../../components/source_loading_page'; interface InfrastructurePageProps extends RouteComponentProps { uiCapabilities: UICapabilities; @@ -95,11 +96,15 @@ export const InfrastructurePage = injectUICapabilities( {({ configuration, createDerivedIndexPattern }) => ( - + {configuration ? ( + + ) : ( + + )} )} diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx index 63f5a81967618d..4db4319b91d3c4 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/index.tsx @@ -11,22 +11,17 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { DocumentTitle } from '../../../components/document_title'; import { MetricsExplorerCharts } from '../../../components/metrics_explorer/charts'; import { MetricsExplorerToolbar } from '../../../components/metrics_explorer/toolbar'; -import { SourceLoadingPage } from '../../../components/source_loading_page'; import { SourceQuery } from '../../../../common/graphql/types'; import { NoData } from '../../../components/empty_states'; import { useMetricsExplorerState } from './use_metric_explorer_state'; import { useTrackPageview } from '../../../hooks/use_track_metric'; interface MetricsExplorerPageProps { - source: SourceQuery.Query['source']['configuration'] | undefined; + source: SourceQuery.Query['source']['configuration']; derivedIndexPattern: IIndexPattern; } export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { - if (!source) { - return ; - } - const { loading, error, diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts index 415a6ae89a8b1b..57ea8861697014 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/metrics_explorer/use_metric_explorer_state.ts @@ -59,7 +59,7 @@ export const useMetricsExplorerState = ( setAfterKey(null); setTimeRange({ ...currentTimerange, from: start, to: end }); }, - [currentTimerange] + [currentTimerange, setTimeRange] ); const handleGroupByChange = useCallback( @@ -70,7 +70,7 @@ export const useMetricsExplorerState = ( groupBy: groupBy || void 0, }); }, - [options] + [options, setOptions] ); const handleFilterQuerySubmit = useCallback( @@ -81,7 +81,7 @@ export const useMetricsExplorerState = ( filterQuery: query, }); }, - [options] + [options, setOptions] ); const handleMetricsChange = useCallback( @@ -92,7 +92,7 @@ export const useMetricsExplorerState = ( metrics, }); }, - [options] + [options, setOptions] ); const handleAggregationChange = useCallback( @@ -109,7 +109,7 @@ export const useMetricsExplorerState = ( })); setOptions({ ...options, aggregation, metrics }); }, - [options] + [options, setOptions] ); const onViewStateChange = useCallback( @@ -124,7 +124,7 @@ export const useMetricsExplorerState = ( setOptions(vs.options); } }, - [setChartOptions, setTimeRange, setTimeRange] + [setChartOptions, setOptions, setTimeRange] ); return { diff --git a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx index 04aa0a9188a57b..85b551a448d0ed 100644 --- a/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/infrastructure/snapshot/page_content.tsx @@ -21,7 +21,7 @@ export const SnapshotPageContent: React.FC = () => ( {({ filterQueryAsJson, applyFilterQuery }) => ( - {({ currentTimeRange, isAutoReloading }) => ( + {({ currentTime }) => ( {({ metric, @@ -33,12 +33,12 @@ export const SnapshotPageContent: React.FC = () => ( boundsOverride, }) => ( ; } +const ITEM_TYPES = inventoryModels.map(m => m.id).join('|'); + export const LinkToPage: React.FC = props => ( { expect(component).toMatchInlineSnapshot(` `); @@ -34,7 +33,6 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -47,7 +45,6 @@ describe('RedirectToLogs component', () => { expect(component).toMatchInlineSnapshot(` `); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx index 7a63406bb419a0..5fa80c8efee73f 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.test.tsx @@ -35,7 +35,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -48,7 +47,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -61,7 +59,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -76,7 +73,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -93,7 +89,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); @@ -108,7 +103,6 @@ describe('RedirectToNodeLogs component', () => { expect(component).toMatchInlineSnapshot(` `); diff --git a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index fe79c169d86212..4af50df3438594 100644 --- a/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -14,9 +14,10 @@ import { LoadingPage } from '../../components/loading_page'; import { replaceLogFilterInQueryString } from '../../containers/logs/with_log_filter'; import { replaceLogPositionInQueryString } from '../../containers/logs/with_log_position'; import { replaceSourceIdInQueryString } from '../../containers/source_id'; -import { InfraNodeType } from '../../graphql/types'; +import { InfraNodeType, SourceConfigurationFields } from '../../graphql/types'; import { getFilterFromLocation, getTimeFromLocation } from './query_params'; import { useSource } from '../../containers/source/source'; +import { findInventoryFields } from '../../../common/inventory_models'; type RedirectToNodeLogsType = RouteComponentProps<{ nodeId: string; @@ -24,6 +25,11 @@ type RedirectToNodeLogsType = RouteComponentProps<{ sourceId?: string; }>; +const getFieldByNodeType = (nodeType: InfraNodeType, fields: SourceConfigurationFields.Fields) => { + const inventoryFields = findInventoryFields(nodeType, fields); + return inventoryFields.id; +}; + export const RedirectToNodeLogs = ({ match: { params: { nodeId, nodeType, sourceId = 'default' }, @@ -50,7 +56,7 @@ export const RedirectToNodeLogs = ({ return null; } - const nodeFilter = `${configuration.fields[nodeType]}: ${nodeId}`; + const nodeFilter = `${getFieldByNodeType(nodeType, configuration.fields)}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx index e62164cb17b2cc..e71985f73fbb8e 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/page_content.tsx @@ -36,7 +36,7 @@ export const LogEntryRatePageContent = () => { useEffect(() => { fetchModuleDefinition(); fetchJobStatus(); - }, []); + }, [fetchJobStatus, fetchModuleDefinition]); if (!hasLogAnalysisCapabilites) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index 2057d75f723543..86760cf2da7d62 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -124,7 +124,7 @@ export const AnomaliesTable: React.FunctionComponent<{ setItemIdToExpandedRowMap(newItemIdToExpandedRowMap); } }, - [results, setTimeRange, timeRange, itemIdToExpandedRowMap, setItemIdToExpandedRowMap] + [itemIdToExpandedRowMap, jobId, results, setTimeRange, timeRange] ); const columns = [ diff --git a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx index 91662c49adace3..5a4c21670191e8 100644 --- a/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/logs/log_entry_rate/setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -54,17 +54,19 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{
); }), - [indices] + [handleCheckboxChange, indices] ); return ( +

+ +

} description={ +

+ +

} description={ { pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: urlTimeRangeRT.encode, urlStateKey: TIME_RANGE_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setTimeRange(timeRange); - }, []); - const [autoRefresh, setAutoRefresh] = useUrlState({ defaultState: { isPaused: false, @@ -56,12 +52,9 @@ export const useLogAnalysisResultsUrlState = () => { pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)), encodeUrlState: autoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, + writeDefaultState: true, }); - useEffect(() => { - setAutoRefresh(autoRefresh); - }, []); - return { timeRange, setTimeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx index 425b5a43f793f7..309961cc390259 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/chart_section_vis.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; import { @@ -42,15 +42,15 @@ export const ChartSectionVis = ({ seriesOverrides, type, }: VisSectionProps) => { - if (!metric || !id) { - return null; - } const [dateFormat] = useKibanaUiSetting('dateFormat'); const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ formatter, formatterTemplate, ]); - const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(metric)), [metric]); + const dateFormatter = useMemo( + () => (metric != null ? niceTimeFormatter(getMaxMinTimestamp(metric)) : undefined), + [metric] + ); const handleTimeChange = useCallback( (from: number, to: number) => { if (onChangeRangeTime) { @@ -73,7 +73,9 @@ export const ChartSectionVis = ({ ), }; - if (!metric) { + if (!id) { + return null; + } else if (!metric) { return ( ); - } - - if (metric.series.some(seriesHasLessThen2DataPoints)) { + } else if (metric.series.some(seriesHasLessThen2DataPoints)) { return ( { - if (!props.metadata) { - return null; - } - const { parsedTimeRange } = props; const { metrics, loading, makeRequest, error } = useNodeDetails( props.requiredMetrics, @@ -65,11 +61,11 @@ export const NodeDetailsPage = (props: Props) => { const refetch = useCallback(() => { makeRequest(); - }, []); + }, [makeRequest]); useEffect(() => { makeRequest(); - }, [parsedTimeRange]); + }, [makeRequest, parsedTimeRange]); if (error) { return ; diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx index 32d2e2eff8ab97..2f9ed9f54df826 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/section.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiTitle } from '@elastic/eui'; import React, { - useContext, Children, - isValidElement, cloneElement, FunctionComponent, - useMemo, + isValidElement, + useContext, } from 'react'; -import { EuiTitle } from '@elastic/eui'; + import { SideNavContext, SubNavItem } from '../lib/side_nav_context'; import { LayoutProps } from '../types'; @@ -31,35 +31,42 @@ export const Section: FunctionComponent = ({ stopLiveStreaming, }) => { const { addNavItem } = useContext(SideNavContext); - const subNavItems: SubNavItem[] = []; - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - const metric = (metrics && metrics.find(m => m.id === child.props.id)) || null; - if (metric) { - subNavItems.push({ - id: child.props.id, - name: child.props.label, - onClick: () => { - const el = document.getElementById(child.props.id); - if (el) { - el.scrollIntoView(); - } - }, - }); - } - return cloneElement(child, { - metrics, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metrics, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] + const subNavItems = Children.toArray(children).reduce( + (accumulatedChildren, child) => { + if (!isValidElement(child)) { + return accumulatedChildren; + } + const metric = metrics?.find(m => m.id === child.props.id) ?? null; + if (metric === null) { + return accumulatedChildren; + } + return [ + ...accumulatedChildren, + { + id: child.props.id, + name: child.props.label, + onClick: () => { + const el = document.getElementById(child.props.id); + if (el) { + el.scrollIntoView(); + } + }, + }, + ]; + }, + [] + ); + + const childrenWithProps = Children.map(children, child => + isValidElement(child) + ? cloneElement(child, { + metrics, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }) + : null ); if (metrics && subNavItems.length) { diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx index f3db3b16701996..325d5102931354 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/components/sub_section.tsx @@ -23,29 +23,25 @@ export const SubSection: FunctionComponent = ({ isLiveStreaming, stopLiveStreaming, }) => { - if (!children || !metrics) { + const metric = useMemo(() => metrics?.find(m => m.id === id), [id, metrics]); + + if (!children || !metric) { return null; } - const metric = metrics.find(m => m.id === id); - if (!metric) { + + const childrenWithProps = Children.map(children, child => { + if (isValidElement(child)) { + return cloneElement(child, { + metric, + id, + onChangeRangeTime, + isLiveStreaming, + stopLiveStreaming, + }); + } return null; - } - const childrenWithProps = useMemo( - () => - Children.map(children, child => { - if (isValidElement(child)) { - return cloneElement(child, { - metric, - id, - onChangeRangeTime, - isLiveStreaming, - stopLiveStreaming, - }); - } - return null; - }), - [children, metric, id, onChangeRangeTime, isLiveStreaming, stopLiveStreaming] - ); + }); + return (
{label ? ( diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx index 432725b6f62b0a..64d2ddb67139d6 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/containers/with_metrics_time.tsx @@ -59,13 +59,10 @@ export const useMetricsTime = () => { const [parsedTimeRange, setParsedTimeRange] = useState(parseRange(defaultRange)); - const updateTimeRange = useCallback( - (range: MetricsTimeInput) => { - setTimeRange(range); - setParsedTimeRange(parseRange(range)); - }, - [setParsedTimeRange] - ); + const updateTimeRange = useCallback((range: MetricsTimeInput) => { + setTimeRange(range); + setParsedTimeRange(parseRange(range)); + }, []); return { timeRange, diff --git a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx index 93253406aec2df..b330ad02f10222 100644 --- a/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/legacy/plugins/infra/public/pages/metrics/index.tsx @@ -112,26 +112,28 @@ export const MetricDetail = withMetricPageProviders( })} /> - + {metadata ? ( + + ) : null} )} diff --git a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts index bb7d253ea15573..a986af07f0c9a0 100644 --- a/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts +++ b/x-pack/legacy/plugins/infra/public/utils/cancellable_effect.ts @@ -27,5 +27,8 @@ export const useCancellableEffect = ( effect(() => cancellationSignal.isCancelled); return cancellationSignal.cancel; + + // the dependencies are managed externally + // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); }; diff --git a/x-pack/legacy/plugins/graph/server/lib/es/index.js b/x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts similarity index 63% rename from x-pack/legacy/plugins/graph/server/lib/es/index.js rename to x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts index b385a1be018c47..391b19d2af91bb 100644 --- a/x-pack/legacy/plugins/graph/server/lib/es/index.js +++ b/x-pack/legacy/plugins/infra/public/utils/formatters/high_precision.ts @@ -4,5 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { callEsSearchApi } from './call_es_search_api'; -export { callEsGraphExploreApi } from './call_es_graph_explore_api'; +export const formatHighPercision = (val: number) => { + return Number(val).toLocaleString('en', { + maximumFractionDigits: 5, + }); +}; diff --git a/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts b/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts index efb20e71a9ce45..3c60dba7478257 100644 --- a/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts +++ b/x-pack/legacy/plugins/infra/public/utils/formatters/index.ts @@ -10,6 +10,7 @@ import { createBytesFormatter } from './bytes'; import { formatNumber } from './number'; import { formatPercent } from './percent'; import { InventoryFormatterType } from '../../../common/inventory_models/types'; +import { formatHighPercision } from './high_precision'; export const FORMATTERS = { number: formatNumber, @@ -21,6 +22,7 @@ export const FORMATTERS = { // bytes in bits formatted string out bits: createBytesFormatter(InfraWaffleMapDataFormat.bitsDecimal), percent: formatPercent, + highPercision: formatHighPercision, }; export const createFormatter = (format: InventoryFormatterType, template: string = '{{value}}') => ( diff --git a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts index c48f95a6521cfb..1b08fb4231243b 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_kibana_ui_setting.ts @@ -28,10 +28,15 @@ import { useObservable } from './use_observable'; export const useKibanaUiSetting = (key: string, defaultValue?: any) => { const uiSettingsClient = npSetup.core.uiSettings; - const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [uiSettingsClient]); + const uiSetting$ = useMemo(() => uiSettingsClient.get$(key, defaultValue), [ + defaultValue, + key, + uiSettingsClient, + ]); const uiSetting = useObservable(uiSetting$); const setUiSetting = useCallback((value: any) => uiSettingsClient.set(key, value), [ + key, uiSettingsClient, ]); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts index 366caf0dfb1563..c23bab7026aaad 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_tracked_promise.ts @@ -190,6 +190,8 @@ export const useTrackedPromise = ( return newPendingPromise.promise; }, + // the dependencies are managed by the caller + // eslint-disable-next-line react-hooks/exhaustive-deps dependencies ); diff --git a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts index d03a5aaa9d697b..79a5d552bcd782 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_url_state.ts @@ -5,10 +5,10 @@ */ import { Location } from 'history'; -import { useMemo, useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { decode, encode, RisonValue } from 'rison-node'; - import { QueryString } from 'ui/utils/query_string'; + import { useHistory } from './history_context'; export const useUrlState = ({ @@ -16,21 +16,26 @@ export const useUrlState = ({ decodeUrlState, encodeUrlState, urlStateKey, + writeDefaultState = false, }: { defaultState: State; decodeUrlState: (value: RisonValue | undefined) => State | undefined; encodeUrlState: (value: State) => RisonValue | undefined; urlStateKey: string; + writeDefaultState?: boolean; }) => { const history = useHistory(); + // history.location is mutable so we can't reliably use useMemo + const queryString = history?.location ? getQueryStringFromLocation(history.location) : ''; + const urlStateString = useMemo(() => { - if (!history) { + if (!queryString) { return; } - return getParamFromQueryString(getQueryStringFromLocation(history.location), urlStateKey); - }, [history && history.location, urlStateKey]); + return getParamFromQueryString(queryString, urlStateKey); + }, [queryString, urlStateKey]); const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ decodeUrlState, @@ -44,27 +49,38 @@ export const useUrlState = ({ const setState = useCallback( (newState: State | undefined) => { - if (!history) { + if (!history || !history.location) { return; } - const location = history.location; + const currentLocation = history.location; const newLocation = replaceQueryStringInLocation( - location, + currentLocation, replaceStateKeyInQueryString( urlStateKey, typeof newState !== 'undefined' ? encodeUrlState(newState) : undefined - )(getQueryStringFromLocation(location)) + )(getQueryStringFromLocation(currentLocation)) ); - if (newLocation !== location) { + if (newLocation !== currentLocation) { history.replace(newLocation); } }, - [encodeUrlState, history, history && history.location, urlStateKey] + [encodeUrlState, history, urlStateKey] ); + const [shouldInitialize, setShouldInitialize] = useState( + writeDefaultState && typeof decodedState === 'undefined' + ); + + useEffect(() => { + if (shouldInitialize) { + setShouldInitialize(false); + setState(defaultState); + } + }, [shouldInitialize, setState, defaultState]); + return [state, setState] as [typeof state, typeof setState]; }; diff --git a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts index 5763834b1cc2a2..f4d8b572e4f7ff 100644 --- a/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts +++ b/x-pack/legacy/plugins/infra/public/utils/use_visibility_state.ts @@ -20,6 +20,6 @@ export const useVisibilityState = (initialState: boolean) => { show, toggle, }), - [isVisible, show, hide] + [hide, isVisible, show, toggle] ); }; diff --git a/x-pack/legacy/plugins/infra/server/graphql/types.ts b/x-pack/legacy/plugins/infra/server/graphql/types.ts index 8f87979dbde2e0..88ad5b2f58f238 100644 --- a/x-pack/legacy/plugins/infra/server/graphql/types.ts +++ b/x-pack/legacy/plugins/infra/server/graphql/types.ts @@ -580,6 +580,10 @@ export enum InfraNodeType { pod = 'pod', container = 'container', host = 'host', + awsEC2 = 'awsEC2', + awsS3 = 'awsS3', + awsRDS = 'awsRDS', + awsSQS = 'awsSQS' } export enum InfraSnapshotMetricType { @@ -590,6 +594,22 @@ export enum InfraSnapshotMetricType { tx = 'tx', rx = 'rx', logRate = 'logRate', + diskIOReadBytes = 'diskIOReadBytes', + diskIOWriteBytes = 'diskIOWriteBytes', + s3TotalRequests = 's3TotalRequests', + s3NumberOfObjects = 's3NumberOfObjects', + s3BucketSize = 's3BucketSize', + s3DownloadBytes = 's3DownloadBytes', + s3UploadBytes = 's3UploadBytes', + rdsConnections = 'rdsConnections', + rdsQueriesExecuted = 'rdsQueriesExecuted', + rdsActiveTransactions = 'rdsActiveTransactions', + rdsLatency = 'rdsLatency', + sqsMessagesVisible = 'sqsOldestMessage', + sqsMessagesDelayed = 'sqsMessagesDelayed', + sqsMessagesSent = 'sqsMessagesSent', + sqsMessagesEmpty = 'sqsMessagesEmpty', + sqsOldestMessage = 'sqsOldestMessage', } export enum InfraMetric { @@ -630,6 +650,24 @@ export enum InfraMetric { awsNetworkPackets = 'awsNetworkPackets', awsDiskioBytes = 'awsDiskioBytes', awsDiskioOps = 'awsDiskioOps', + awsEC2CpuUtilization = 'awsEC2CpuUtilization', + awsEC2DiskIOBytes = 'awsEC2DiskIOBytes', + awsEC2NetworkTraffic = 'awsEC2NetworkTraffic', + awsS3TotalRequests = 'awsS3TotalRequests', + awsS3NumberOfObjects = 'awsS3NumberOfObjects', + awsS3BucketSize = 'awsS3BucketSize', + awsS3DownloadBytes = 'awsS3DownloadBytes', + awsS3UploadBytes = 'awsS3UploadBytes', + awsRDSCpuTotal = 'awsRDSCpuTotal', + awsRDSConnections = 'awsRDSConnections', + awsRDSQueriesExecuted = 'awsRDSQueriesExecuted', + awsRDSActiveTransactions = 'awsRDSActiveTransactions', + awsRDSLatency = 'awsRDSLatency', + awsSQSMessagesVisible = 'awsSQSMessagesVisible', + awsSQSMessagesDelayed = 'awsSQSMessagesDelayed', + awsSQSMessagesSent = 'awsSQSMessagesSent', + awsSQSMessagesEmpty = 'awsSQSMessagesEmpty', + awsSQSOldestMessage = 'awsSQSOldestMessage', custom = 'custom', } diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts index e88736b08b95b9..a0fd6d3e951bfb 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -9,7 +9,7 @@ import { Lifecycle } from 'hapi'; import { ObjectType } from '@kbn/config-schema'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { RouteMethod, RouteConfig } from '../../../../../../../../src/core/server'; -import { APMPluginContract } from '../../../../../../../plugins/apm/server/plugin'; +import { APMPluginContract } from '../../../../../../../plugins/apm/server'; // NP_TODO: Compose real types from plugins we depend on, no "any" export interface InfraServerPluginDeps { diff --git a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index db3c516841cd4a..c4146f5758d80c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/legacy/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -7,11 +7,11 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; -import { InfraMetric, InfraMetricData, InfraNodeType } from '../../../graphql/types'; +import { InfraMetric, InfraMetricData } from '../../../graphql/types'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; import { checkValidNode } from './lib/check_valid_node'; -import { metrics } from '../../../../common/inventory_models'; +import { metrics, findInventoryFields } from '../../../../common/inventory_models'; import { TSVBMetricModelCreator } from '../../../../common/inventory_models/types'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; @@ -27,13 +27,10 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { options: InfraMetricsRequestOptions, rawRequest: KibanaRequest // NP_TODO: Temporarily needed until metrics getVisData no longer needs full request ): Promise { - const fields = { - [InfraNodeType.host]: options.sourceConfiguration.fields.host, - [InfraNodeType.container]: options.sourceConfiguration.fields.container, - [InfraNodeType.pod]: options.sourceConfiguration.fields.pod, - }; const indexPattern = `${options.sourceConfiguration.metricAlias},${options.sourceConfiguration.logAlias}`; - const nodeField = fields[options.nodeType]; + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + const nodeField = fields.id; + const search = (searchOptions: object) => this.framework.callWithRequest<{}, Aggregation>(requestContext, 'search', searchOptions); diff --git a/x-pack/legacy/plugins/infra/server/lib/constants.ts b/x-pack/legacy/plugins/infra/server/lib/constants.ts index 4f2fa561da0c50..0765256c4160c3 100644 --- a/x-pack/legacy/plugins/infra/server/lib/constants.ts +++ b/x-pack/legacy/plugins/infra/server/lib/constants.ts @@ -4,21 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { InfraNodeType } from '../graphql/types'; - -// Used for metadata and snapshots resolvers to find the field that contains -// a displayable name of a node. -// Intentionally not the same as xpack.infra.sources.default.fields.{host,container,pod}. -// TODO: consider moving this to source configuration too. -export const NAME_FIELDS = { - [InfraNodeType.host]: 'host.name', - [InfraNodeType.pod]: 'kubernetes.pod.name', - [InfraNodeType.container]: 'container.name', -}; -export const IP_FIELDS = { - [InfraNodeType.host]: 'host.ip', - [InfraNodeType.pod]: 'kubernetes.pod.ip', - [InfraNodeType.container]: 'container.ip_address', -}; - export const CLOUD_METRICS_MODULES = ['aws']; diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts new file mode 100644 index 00000000000000..6c27e54a78bee4 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts @@ -0,0 +1,59 @@ +/* + * 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 { uniq } from 'lodash'; +import { RequestHandlerContext } from 'kibana/server'; +import { InfraSnapshotRequestOptions } from './types'; +import { InfraTimerangeInput } from '../../../public/graphql/types'; +import { getMetricsAggregations } from './query_helpers'; +import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; +import { SnapshotModel, SnapshotModelMetricAggRT } from '../../../common/inventory_models/types'; +import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; + +export const createTimeRangeWithInterval = async ( + framework: KibanaFramework, + requestContext: RequestHandlerContext, + options: InfraSnapshotRequestOptions +): Promise => { + const aggregations = getMetricsAggregations(options); + const modules = aggregationsToModules(aggregations); + const interval = + (await calculateMetricInterval( + framework, + requestContext, + { + indexPattern: options.sourceConfiguration.metricAlias, + timestampField: options.sourceConfiguration.fields.timestamp, + timerange: { from: options.timerange.from, to: options.timerange.to }, + }, + modules, + options.nodeType + )) || 60000; + return { + interval: `${interval}s`, + from: options.timerange.to - interval * 5000, // We need at least 5 buckets worth of data + to: options.timerange.to, + }; +}; + +const aggregationsToModules = (aggregations: SnapshotModel): string[] => { + return uniq( + Object.values(aggregations) + .reduce((modules, agg) => { + if (SnapshotModelMetricAggRT.is(agg)) { + return modules.concat(Object.values(agg).map(a => a?.field)); + } + return modules; + }, [] as Array) + .filter(v => v) + .map(field => + field! + .split(/\./) + .slice(0, 2) + .join('.') + ) + ) as string[]; +}; diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts index 6ebbe8775562c7..44d32c7b915a8c 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/query_helpers.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { findInventoryModel } from '../../../common/inventory_models/index'; -import { InfraSnapshotRequestOptions } from './snapshot'; -import { NAME_FIELDS } from '../constants'; +import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index'; +import { InfraSnapshotRequestOptions } from './types'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; import { SnapshotModelRT, SnapshotModel } from '../../../common/inventory_models/types'; @@ -21,28 +19,35 @@ interface GroupBySource { }; } +export const getFieldByNodeType = (options: InfraSnapshotRequestOptions) => { + const inventoryFields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + return inventoryFields.id; +}; + export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) => { + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); const sources: GroupBySource[] = options.groupBy.map(gb => { return { [`${gb.field}`]: { terms: { field: gb.field } } }; }); sources.push({ id: { - terms: { field: options.sourceConfiguration.fields[options.nodeType] }, + terms: { field: fields.id }, }, }); sources.push({ - name: { terms: { field: NAME_FIELDS[options.nodeType], missing_bucket: true } }, + name: { terms: { field: fields.name, missing_bucket: true } }, }); return sources; }; export const getMetricsSources = (options: InfraSnapshotRequestOptions) => { - return [{ id: { terms: { field: options.sourceConfiguration.fields[options.nodeType] } } }]; + const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + return [{ id: { terms: { field: fields.id } } }]; }; export const getMetricsAggregations = (options: InfraSnapshotRequestOptions): SnapshotModel => { - const model = findInventoryModel(options.nodeType); - const aggregation = get(model, ['metrics', 'snapshot', options.metric.type]); + const inventoryModel = findInventoryModel(options.nodeType); + const aggregation = inventoryModel.metrics.snapshot?.[options.metric.type]; if (!SnapshotModelRT.is(aggregation)) { throw new Error( i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', { diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts index 6b18d9489c100b..d22f41ff152f76 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/response_helpers.ts @@ -14,8 +14,8 @@ import { InfraNodeType, } from '../../graphql/types'; import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; -import { InfraSnapshotRequestOptions } from './snapshot'; -import { IP_FIELDS } from '../constants'; +import { InfraSnapshotRequestOptions } from './types'; +import { findInventoryModel } from '../../../common/inventory_models'; export interface InfraSnapshotNodeMetricsBucket { key: { id: string }; @@ -73,12 +73,13 @@ export const getIPFromBucket = ( nodeType: InfraNodeType, bucket: InfraSnapshotNodeGroupByBucket ): string | null => { - const ip = get( - bucket, - `ip.hits.hits[0]._source.${IP_FIELDS[nodeType]}`, - null - ); - + const inventoryModel = findInventoryModel(nodeType); + if (!inventoryModel.fields.ip) { + return null; + } + const ip = get(bucket, `ip.hits.hits[0]._source.${inventoryModel.fields.ip}`, null) as + | string[] + | null; if (Array.isArray(ip)) { return ip.find(isIPv4) || null; } else if (typeof ip === 'string') { diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts index 95769414832cc0..d1db0ef07b3384 100644 --- a/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/snapshot.ts @@ -5,14 +5,7 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { - InfraSnapshotGroupbyInput, - InfraSnapshotMetricInput, - InfraSnapshotNode, - InfraTimerangeInput, - InfraNodeType, - InfraSourceConfiguration, -} from '../../graphql/types'; +import { InfraSnapshotNode } from '../../graphql/types'; import { InfraDatabaseSearchResponse } from '../adapters/framework'; import { KibanaFramework } from '../adapters/framework/kibana_framework_adapter'; import { InfraSources } from '../sources'; @@ -32,18 +25,11 @@ import { InfraSnapshotNodeGroupByBucket, InfraSnapshotNodeMetricsBucket, } from './response_helpers'; -import { IP_FIELDS } from '../constants'; import { getAllCompositeData } from '../../utils/get_all_composite_data'; import { createAfterKeyHandler } from '../../utils/create_afterkey_handler'; - -export interface InfraSnapshotRequestOptions { - nodeType: InfraNodeType; - sourceConfiguration: InfraSourceConfiguration; - timerange: InfraTimerangeInput; - groupBy: InfraSnapshotGroupbyInput[]; - metric: InfraSnapshotMetricInput; - filterQuery: JsonObject | undefined; -} +import { findInventoryModel } from '../../../common/inventory_models'; +import { InfraSnapshotRequestOptions } from './types'; +import { createTimeRangeWithInterval } from './create_timerange_with_interval'; export class InfraSnapshot { constructor(private readonly libs: { sources: InfraSources; framework: KibanaFramework }) {} @@ -56,8 +42,22 @@ export class InfraSnapshot { // in order to page through the results of their respective composite aggregations. // Both chains of requests are supposed to run in parallel, and their results be merged // when they have both been completed. - const groupedNodesPromise = requestGroupedNodes(requestContext, options, this.libs.framework); - const nodeMetricsPromise = requestNodeMetrics(requestContext, options, this.libs.framework); + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval( + this.libs.framework, + requestContext, + options + ); + const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; + const groupedNodesPromise = requestGroupedNodes( + requestContext, + optionsWithTimerange, + this.libs.framework + ); + const nodeMetricsPromise = requestNodeMetrics( + requestContext, + optionsWithTimerange, + this.libs.framework + ); const groupedNodeBuckets = await groupedNodesPromise; const nodeMetricBuckets = await nodeMetricsPromise; @@ -79,6 +79,7 @@ const requestGroupedNodes = async ( options: InfraSnapshotRequestOptions, framework: KibanaFramework ): Promise => { + const inventoryModel = findInventoryModel(options.nodeType); const query = { allowNoIndices: true, index: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, @@ -112,7 +113,7 @@ const requestGroupedNodes = async ( top_hits: { sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }], _source: { - includes: [IP_FIELDS[options.nodeType]], + includes: inventoryModel.fields.ip ? [inventoryModel.fields.ip] : [], }, size: 1, }, diff --git a/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts b/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts new file mode 100644 index 00000000000000..778f5045894a90 --- /dev/null +++ b/x-pack/legacy/plugins/infra/server/lib/snapshot/types.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { JsonObject } from '../../../common/typed_json'; +import { + InfraNodeType, + InfraSourceConfiguration, + InfraTimerangeInput, + InfraSnapshotGroupbyInput, + InfraSnapshotMetricInput, +} from '../../../public/graphql/types'; + +export interface InfraSnapshotRequestOptions { + nodeType: InfraNodeType; + sourceConfiguration: InfraSourceConfiguration; + timerange: InfraTimerangeInput; + groupBy: InfraSnapshotGroupbyInput[]; + metric: InfraSnapshotMetricInput; + filterQuery: JsonObject | undefined; +} diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts deleted file mode 100644 index 5f6bdd30fa2b88..00000000000000 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_id_field_name.ts +++ /dev/null @@ -1,18 +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 { InfraSourceConfiguration } from '../../../lib/sources'; - -export const getIdFieldName = (sourceConfiguration: InfraSourceConfiguration, nodeType: string) => { - switch (nodeType) { - case 'host': - return sourceConfiguration.fields.host; - case 'container': - return sourceConfiguration.fields.container; - default: - return sourceConfiguration.fields.pod; - } -}; diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index 3bd22062c26a07..191339565b813f 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -12,8 +12,8 @@ import { } from '../../../lib/adapters/framework'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; -import { NAME_FIELDS } from '../../../lib/constants'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export interface InfraMetricsAdapterResponse { id: string; @@ -26,10 +26,9 @@ export const getMetricMetadata = async ( requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ): Promise => { - const idFieldName = getIdFieldName(sourceConfiguration, nodeType); - + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const metricQuery = { allowNoIndices: true, ignoreUnavailable: true, @@ -40,7 +39,7 @@ export const getMetricMetadata = async ( must_not: [{ match: { 'event.dataset': 'aws.ec2' } }], filter: [ { - match: { [idFieldName]: nodeId }, + match: { [fields.id]: nodeId }, }, ], }, @@ -49,7 +48,7 @@ export const getMetricMetadata = async ( aggs: { nodeName: { terms: { - field: NAME_FIELDS[nodeType], + field: fields.name, size: 1, }, }, diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 1567b6d1bd1ec7..4ff0df30abedd8 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { first } from 'lodash'; +import { first, set, startsWith } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; @@ -12,14 +12,15 @@ import { InfraNodeType } from '../../../graphql/types'; import { InfraMetadataInfo } from '../../../../common/http_api/metadata_api'; import { getPodNodeName } from './get_pod_node_name'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export const getNodeInfo = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ): Promise => { // If the nodeType is a Kubernetes pod then we need to get the node info // from a host record instead of a pod. This is due to the fact that any host @@ -45,6 +46,7 @@ export const getNodeInfo = async ( } return {}; } + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -55,12 +57,18 @@ export const getNodeInfo = async ( _source: ['host.*', 'cloud.*'], query: { bool: { - must_not: CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })), - filter: [{ match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }], + filter: [{ match: { [fields.id]: nodeId } }], }, }, }, }; + if (!CLOUD_METRICS_MODULES.some(m => startsWith(nodeType, m))) { + set( + params, + 'body.query.bool.must_not', + CLOUD_METRICS_MODULES.map(module => ({ match: { 'event.module': module } })) + ); + } const response = await framework.callWithRequest<{ _source: InfraMetadataInfo }, {}>( requestContext, 'search', diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 47ffc7f83b6bc2..be6e29a794d096 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -8,7 +8,7 @@ import { first, get } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; export const getPodNodeName = async ( framework: KibanaFramework, @@ -17,6 +17,7 @@ export const getPodNodeName = async ( nodeId: string, nodeType: 'host' | 'pod' | 'container' ): Promise => { + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); const params = { allowNoIndices: true, ignoreUnavailable: true, @@ -28,7 +29,7 @@ export const getPodNodeName = async ( query: { bool: { filter: [ - { match: { [getIdFieldName(sourceConfiguration, nodeType)]: nodeId } }, + { match: { [fields.id]: nodeId } }, { exists: { field: `kubernetes.node.name` } }, ], }, diff --git a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts index ab242804173c03..9ca0819d74d466 100644 --- a/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts +++ b/x-pack/legacy/plugins/infra/server/routes/metadata/lib/has_apm_data.ts @@ -8,24 +8,25 @@ import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; -import { getIdFieldName } from './get_id_field_name'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { InventoryItemType } from '../../../../common/inventory_models/types'; export const hasAPMData = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, sourceConfiguration: InfraSourceConfiguration, nodeId: string, - nodeType: 'host' | 'pod' | 'container' + nodeType: InventoryItemType ) => { const apmIndices = await framework.plugins.apm.getApmIndices( requestContext.core.savedObjects.client ); const apmIndex = apmIndices['apm_oss.transactionIndices'] || 'apm-*'; + const fields = findInventoryFields(nodeType, sourceConfiguration.fields); // There is a bug in APM ECS data where host.name is not set. // This will fixed with: https://github.com/elastic/apm-server/issues/2502 - const nodeFieldName = - nodeType === 'host' ? 'host.hostname' : getIdFieldName(sourceConfiguration, nodeType); + const nodeFieldName = nodeType === 'host' ? 'host.hostname' : fields.id; const params = { allowNoIndices: true, ignoreUnavailable: true, diff --git a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts index 013a261d24831d..ae707bae79b9ea 100644 --- a/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/legacy/plugins/infra/server/routes/snapshot/index.ts @@ -9,12 +9,12 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { InfraSnapshotRequestOptions } from '../../lib/snapshot'; import { UsageCollector } from '../../usage/usage_collector'; import { parseFilterQuery } from '../../utils/serialized_query'; import { InfraNodeType, InfraSnapshotMetricInput } from '../../../public/graphql/types'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; +import { InfraSnapshotRequestOptions } from '../../lib/snapshot/types'; const escapeHatch = schema.object({}, { allowUnknowns: true }); diff --git a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts index 5eb5d424cdd737..6247c0f0298a0c 100644 --- a/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/legacy/plugins/infra/server/utils/calculate_metric_interval.ts @@ -5,6 +5,8 @@ */ import { RequestHandlerContext } from 'src/core/server'; +import { InfraNodeType } from '../graphql/types'; +import { findInventoryModel } from '../../common/inventory_models'; import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; interface Options { @@ -24,8 +26,14 @@ export const calculateMetricInterval = async ( framework: KibanaFramework, requestContext: RequestHandlerContext, options: Options, - modules: string[] + modules: string[], + nodeType?: InfraNodeType // TODO: check that this type still makes sense ) => { + let from = options.timerange.from; + if (nodeType) { + const inventoryModel = findInventoryModel(nodeType); + from = options.timerange.to - inventoryModel.metrics.defaultTimeRangeInSeconds * 1000; + } const query = { allowNoIndices: true, index: options.indexPattern, @@ -37,7 +45,7 @@ export const calculateMetricInterval = async ( { range: { [options.timestampField]: { - gte: options.timerange.from, + gte: from, lte: options.timerange.to, format: 'epoch_millis', }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index cdde03d7e9f56d..05dcafcaeba319 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -162,7 +162,7 @@ export function InnerWorkspacePanel({

{ } `); }); + + it('should handle this week now/w', () => { + const { dateRange } = mergeTables.fn( + { + type: 'kibana_context', + timeRange: { + from: 'now/w', + to: 'now/w', + }, + }, + { layerIds: ['first', 'second'], tables: [] }, + {} + ); + + expect( + moment + .duration( + moment() + .startOf('week') + .diff(dateRange!.fromDate) + ) + .asDays() + ).toEqual(0); + + expect( + moment + .duration( + moment() + .endOf('week') + .diff(dateRange!.toDate) + ) + .asDays() + ).toEqual(0); + }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index 5f47898b4f6321..dc03be894a87cd 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -5,9 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import dateMath from '@elastic/datemath'; import { ExpressionFunction, KibanaContext, KibanaDatatable } from 'src/plugins/expressions/public'; import { LensMultiTable } from '../types'; +import { toAbsoluteDates } from '../indexpattern_plugin/auto_date'; interface MergeTables { layerIds: string[]; @@ -58,15 +58,11 @@ function getDateRange(ctx?: KibanaContext | null) { return; } - const fromDate = dateMath.parse(ctx.timeRange.from); - const toDate = dateMath.parse(ctx.timeRange.to); + const dateRange = toAbsoluteDates({ fromDate: ctx.timeRange.from, toDate: ctx.timeRange.to }); - if (!fromDate || !toDate) { + if (!dateRange) { return; } - return { - fromDate: fromDate.toDate(), - toDate: toDate.toDate(), - }; + return dateRange; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts index dec0adba98103a..b62585f5da09ad 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/auto_date.ts @@ -16,16 +16,36 @@ interface LensAutoDateProps { aggConfigs: string; } -export function autoIntervalFromDateRange(dateRange?: DateRange, defaultValue: string = '1h') { +export function toAbsoluteDates(dateRange?: DateRange) { if (!dateRange) { + return; + } + + const fromDate = dateMath.parse(dateRange.fromDate); + const toDate = dateMath.parse(dateRange.toDate, { roundUp: true }); + + if (!fromDate || !toDate) { + return; + } + + return { + fromDate: fromDate.toDate(), + toDate: toDate.toDate(), + }; +} + +export function autoIntervalFromDateRange(dateRange?: DateRange, defaultValue: string = '1h') { + const dates = toAbsoluteDates(dateRange); + if (!dates) { return defaultValue; } const buckets = new TimeBuckets(); + buckets.setInterval('auto'); buckets.setBounds({ - min: dateMath.parse(dateRange.fromDate), - max: dateMath.parse(dateRange.toDate, { roundUp: true }), + min: dates.fromDate, + max: dates.toDate, }); return buckets.getInterval().expression; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index dc23df250ebd47..52f00a7cd4e9df 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -278,7 +278,7 @@ describe('IndexPattern Data Panel', () => { function testProps() { const setState = jest.fn(); - core.http.get = jest.fn(async (url: string) => { + core.http.get.mockImplementation(async (url: string) => { const parts = url.split('/'); const indexPatternTitle = parts[parts.length - 1]; return { @@ -484,7 +484,7 @@ describe('IndexPattern Data Panel', () => { let overlapCount = 0; const props = testProps(); - core.http.get = jest.fn((url: string) => { + core.http.get.mockImplementation((url: string) => { if (queryCount) { ++overlapCount; } @@ -533,11 +533,9 @@ describe('IndexPattern Data Panel', () => { it('shows all fields if empty state button is clicked', async () => { const props = testProps(); - core.http.get = jest.fn((url: string) => { - return Promise.resolve({ - indexPatternTitle: props.currentIndexPatternId, - existingFieldNames: [], - }); + core.http.get.mockResolvedValue({ + indexPatternTitle: props.currentIndexPatternId, + existingFieldNames: [], }); const inst = mountWithIntl(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx index 8fc80e14dc166a..6a2f6234279c70 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.tsx @@ -298,6 +298,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({ 'data-test-subj': 'indexPattern-switch-link', className: 'lnsInnerIndexPatternDataPanel__triggerButton', }} + indexPatternId={currentIndexPatternId} indexPatternRefs={indexPatternRefs} onChangeIndexPattern={(newId: string) => { onChangeIndexPattern(newId); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts index 72cbd1b861a05e..2fb678aed5a54b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.test.ts @@ -528,7 +528,8 @@ describe('loader', () => { await syncExistingFields({ dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' }, - fetchJson, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fetchJson: fetchJson as any, indexPatterns: [{ title: 'a' }, { title: 'b' }, { title: 'c' }], setState, }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index ad3d3f3816262e..28486c8201da0f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -32,8 +32,8 @@ describe('state_helpers', () => { operationType: 'terms', sourceField: 'source', params: { - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', + orderBy: { type: 'column', columnId: 'col2' }, + orderDirection: 'desc', size: 5, }, }; @@ -65,11 +65,18 @@ describe('state_helpers', () => { expect( deleteColumn({ state, columnId: 'col2', layerId: 'first' }).layers.first.columns ).toEqual({ - col1: termsColumn, + col1: { + ...termsColumn, + params: { + ...termsColumn.params, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + }, }); }); - it('should execute adjustments for other columns', () => { + it('should adjust when deleting other columns', () => { const termsColumn: TermsIndexPatternColumn = { label: 'Top values of source', dataType: 'string', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index db44d73a003370..f56f8089ea586a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -124,11 +124,10 @@ export function deleteColumn({ layerId: string; columnId: string; }): IndexPatternPrivateState { - const newColumns = adjustColumnReferencesForChangedColumn( - state.layers[layerId].columns, - columnId - ); - delete newColumns[columnId]; + const hypotheticalColumns = { ...state.layers[layerId].columns }; + delete hypotheticalColumns[columnId]; + + const newColumns = adjustColumnReferencesForChangedColumn(hypotheticalColumns, columnId); return { ...state, diff --git a/x-pack/legacy/plugins/lens/server/usage/task.ts b/x-pack/legacy/plugins/lens/server/usage/task.ts index feb73538f44f05..64bc37ce909a28 100644 --- a/x-pack/legacy/plugins/lens/server/usage/task.ts +++ b/x-pack/legacy/plugins/lens/server/usage/task.ts @@ -15,7 +15,7 @@ import { DeleteDocumentByQueryResponse, } from 'elasticsearch'; import { ESSearchResponse } from '../../../apm/typings/elasticsearch'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { RunContext } from '../../../task_manager'; import { getVisualizationCounts } from './visualization_counts'; diff --git a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts index abd658ff91db04..1da3c942830ca7 100644 --- a/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/license_management/public/np_ready/plugin.ts @@ -5,7 +5,7 @@ */ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { PLUGIN } from '../../common/constants'; import { Breadcrumb } from './application/breadcrumbs'; diff --git a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts b/x-pack/legacy/plugins/license_management/server/np_ready/types.ts index a636481323e29f..0e66946ec1cc6c 100644 --- a/x-pack/legacy/plugins/license_management/server/np_ready/types.ts +++ b/x-pack/legacy/plugins/license_management/server/np_ready/types.ts @@ -5,7 +5,7 @@ */ import { IRouter } from 'src/core/server'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { ElasticsearchPlugin } from '../../../../../../src/legacy/core_plugins/elasticsearch'; export interface Dependencies { diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index e5b3d5c6150131..4f5bca6af7feb1 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -28,7 +28,7 @@ export function maps(kibana) { description: i18n.translate('xpack.maps.appDescription', { defaultMessage: 'Map application' }), - main: 'plugins/maps/index', + main: 'plugins/maps/legacy', icon: 'plugins/maps/icon.svg', euiIconType: APP_ICON, }, diff --git a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js index fdf5172fea8ca5..cff6fe878c9fca 100644 --- a/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js +++ b/x-pack/legacy/plugins/maps/public/angular/services/gis_map_saved_object_loader.js @@ -6,12 +6,12 @@ import './saved_gis_map'; import { uiModules } from 'ui/modules'; -import { SavedObjectLoader, SavedObjectsClientProvider } from 'ui/saved_objects'; +import { SavedObjectLoader } from 'ui/saved_objects'; +import { npStart } from '../../../../../../../src/legacy/ui/public/new_platform'; const module = uiModules.get('app/maps'); // This is the only thing that gets injected into controllers -module.service('gisMapSavedObjectLoader', function (Private, SavedGisMap, kbnUrl, chrome) { - const savedObjectClient = Private(SavedObjectsClientProvider); - return new SavedObjectLoader(SavedGisMap, kbnUrl, chrome, savedObjectClient); +module.service('gisMapSavedObjectLoader', function (SavedGisMap) { + return new SavedObjectLoader(SavedGisMap, npStart.core.savedObjects.client, npStart.core.chrome); }); diff --git a/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap b/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap index 4524f66c0642c8..e21f034161a878 100644 --- a/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/components/__snapshots__/tooltip_selector.test.js.snap @@ -2,20 +2,6 @@ exports[`TooltipSelector should render component 1`] = `
- -
- -
-
- diff --git a/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js b/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js index e841fa573c9a54..56406ee9653fe1 100644 --- a/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js +++ b/x-pack/legacy/plugins/maps/public/components/global_filter_checkbox.js @@ -6,13 +6,8 @@ import React from 'react'; import { EuiFormRow, EuiSwitch } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -const label = i18n.translate('xpack.maps.layerPanel.applyGlobalQueryCheckboxLabel', { - defaultMessage: `Apply global filter to source`, -}); - -export function GlobalFilterCheckbox({ applyGlobalQuery, customLabel, setApplyGlobalQuery }) { +export function GlobalFilterCheckbox({ applyGlobalQuery, label, setApplyGlobalQuery }) { const onApplyGlobalQueryChange = event => { setApplyGlobalQuery(event.target.checked); }; @@ -22,7 +17,7 @@ export function GlobalFilterCheckbox({ applyGlobalQuery, customLabel, setApplyGl display="columnCompressedSwitch" > - -
- -
-
- - {this._renderProperties()} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap index 4377fa47254835..101716d297b81e 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap @@ -97,7 +97,9 @@ exports[`LayerPanel is rendered 1`] = ` > - +
+ mockSourceSettings +
diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js index 0086c5067ba123..fc47ea6c36a012 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js @@ -16,16 +16,17 @@ import { EuiTextColor, EuiTextAlign, EuiButtonEmpty, + EuiFormRow, + EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { indexPatternService } from '../../../kibana_services'; - -import { start as data } from '../../../../../../../../src/legacy/core_plugins/data/public/legacy'; -const { SearchBar } = data.ui; +import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; export class FilterEditor extends Component { state = { @@ -79,6 +80,14 @@ export class FilterEditor extends Component { this._close(); }; + _onFilterByMapBoundsChange = event => { + this.props.updateSourceProp(this.props.layer.getId(), 'filterByMapBounds', event.target.checked); + }; + + _onApplyGlobalQueryChange = applyGlobalQuery => { + this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalQuery', applyGlobalQuery); + }; + _renderQueryPopover() { const layerQuery = this.props.layer.getQuery(); const { uiSettings } = npStart.core; @@ -169,13 +178,29 @@ export class FilterEditor extends Component { } render() { + let filterByBoundsSwitch; + if (this.props.layer.getSource().isFilterByMapBoundsConfigurable()) { + filterByBoundsSwitch = ( + + + + ); + } + return (
@@ -185,6 +210,18 @@ export class FilterEditor extends Component { {this._renderQuery()} {this._renderQueryPopover()} + + + + {filterByBoundsSwitch} + +
); } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js index 4fc69690485fb9..127f2ca70ab935 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/filter_editor/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { FilterEditor } from './filter_editor'; import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { setLayerQuery } from '../../../actions/map_actions'; +import { setLayerQuery, updateSourceProp } from '../../../actions/map_actions'; function mapStateToProps(state = {}) { return { @@ -19,7 +19,8 @@ function mapDispatchToProps(dispatch) { return { setLayerQuery: (layerId, query) => { dispatch(setLayerQuery(layerId, query)); - } + }, + updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js index ebdeb22289a9af..85fdcc90278542 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/index.js @@ -8,7 +8,8 @@ import { connect } from 'react-redux'; import { LayerPanel } from './view'; import { getSelectedLayer } from '../../selectors/map_selectors'; import { - fitToLayerExtent + fitToLayerExtent, + updateSourceProp, } from '../../actions/map_actions'; function mapStateToProps(state = {}) { @@ -21,7 +22,8 @@ function mapDispatchToProps(dispatch) { return { fitToBounds: (layerId) => { dispatch(fitToLayerExtent(layerId)); - } + }, + updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js index 68d4656880666c..b23764f1c7e338 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js @@ -217,7 +217,7 @@ export class Join extends Component { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js index fb09ed342b8d35..4fc1eba1f1e2fa 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js @@ -13,8 +13,8 @@ import { EuiFormHelpText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SearchBar } from 'plugins/data'; import { npStart } from 'ui/new_platform'; +const { SearchBar } = npStart.plugins.data.ui; export class WhereExpression extends Component { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/__snapshots__/source_settings.test.js.snap b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/__snapshots__/source_settings.test.js.snap deleted file mode 100644 index 4d2cbcb012b411..00000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/__snapshots__/source_settings.test.js.snap +++ /dev/null @@ -1,30 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should render source settings editor 1`] = ` - - - -
- -
-
- -
- mockSourceEditor -
-
- -
-`; - -exports[`should render nothing when source has no editor 1`] = `""`; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/index.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/index.js deleted file mode 100644 index 18cda96aeb1e88..00000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/index.js +++ /dev/null @@ -1,25 +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 { connect } from 'react-redux'; -import { SourceSettings } from './source_settings'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; -import { updateSourceProp } from '../../../actions/map_actions'; - -function mapStateToProps(state = {}) { - return { - layer: getSelectedLayer(state) - }; -} - -function mapDispatchToProps(dispatch) { - return { - updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), - }; -} - -const connectedSourceSettings = connect(mapStateToProps, mapDispatchToProps)(SourceSettings); -export { connectedSourceSettings as SourceSettings }; diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.js deleted file mode 100644 index 9791931c3ee77c..00000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Fragment } from 'react'; - -import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export function SourceSettings({ layer, updateSourceProp }) { - const onSourceChange = ({ propName, value }) => { - updateSourceProp(layer.getId(), propName, value); - }; - - const sourceSettingsEditor = layer.renderSourceSettingsEditor({ onChange: onSourceChange }); - - if (!sourceSettingsEditor) { - return null; - } - - return ( - - - -
- -
-
- - - - {sourceSettingsEditor} -
- - -
- ); -} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.test.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.test.js deleted file mode 100644 index 090d30054ba814..00000000000000 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/source_settings/source_settings.test.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { SourceSettings } from './source_settings'; - -test('Should render source settings editor', () => { - const mockLayer = { - renderSourceSettingsEditor: () => { - return (
mockSourceEditor
); - }, - }; - const component = shallow( - - ); - - expect(component) - .toMatchSnapshot(); -}); - -test('should render nothing when source has no editor', () => { - const mockLayer = { - renderSourceSettingsEditor: () => { - return null; - }, - }; - const component = shallow( - - ); - - expect(component) - .toMatchSnapshot(); -}); diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js index 78cb8aa827e35b..492c891d1db2d5 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.js @@ -11,7 +11,6 @@ import { JoinEditor } from './join_editor'; import { FlyoutFooter } from './flyout_footer'; import { LayerErrors } from './layer_errors'; import { LayerSettings } from './layer_settings'; -import { SourceSettings } from './source_settings'; import { StyleSettings } from './style_settings'; import { EuiButtonIcon, @@ -96,6 +95,10 @@ export class LayerPanel extends React.Component { } } + _onSourceChange = ({ propName, value }) => { + this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value); + }; + _renderFilterSection() { if (!this.props.selectedLayer.supportsElasticsearchFilters()) { return null; @@ -213,7 +216,7 @@ export class LayerPanel extends React.Component { - + {this.props.selectedLayer.renderSourceSettingsEditor({ onChange: this._onSourceChange })} {this._renderFilterSection()} diff --git a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js index fe891d92defbe8..8e97c58b695083 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/layer_panel/view.test.js @@ -40,12 +40,6 @@ jest.mock('./layer_settings', () => ({ } })); -jest.mock('./source_settings', () => ({ - SourceSettings: () => { - return (
mockSourceSettings
); - } -})); - import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; @@ -62,11 +56,13 @@ const mockLayer = { isJoinable: () => { return true; }, supportsElasticsearchFilters: () => { return false; }, getLayerTypeIconName: () => { return 'vector'; }, + renderSourceSettingsEditor: () => { return (
mockSourceSettings
); }, }; const defaultProps = { selectedLayer: mockLayer, fitToBounds: () => {}, + updateSourceProp: () => {}, }; describe('LayerPanel', () => { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/README.md b/x-pack/legacy/plugins/maps/public/embeddable/README.md index c2952de82c223f..82f83f1bfcf4a2 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/README.md +++ b/x-pack/legacy/plugins/maps/public/embeddable/README.md @@ -79,3 +79,101 @@ const eventHandlers = { const mapEmbeddable = await factory.createFromState(state, input, parent, renderTooltipContent, eventHandlers); ``` + + +#### Passing in geospatial data +You can pass geospatial data into the Map embeddable by configuring the layerList parameter with a layer with `GEOJSON_FILE` source. +Geojson sources will not update unless you modify `__featureCollection` property by calling the `setLayerList` method. + +``` +const factory = new MapEmbeddableFactory(); +const state = { + layerList: [ + { + 'id': 'gaxya', + 'label': 'My geospatial data', + 'minZoom': 0, + 'maxZoom': 24, + 'alpha': 1, + 'sourceDescriptor': { + 'id': 'b7486', + 'type': 'GEOJSON_FILE', + '__featureCollection': { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [0, 0], [10, 10], [10, 0], [0, 0] + ] + ] + }, + "properties": { + "name": "null island", + "another_prop": "something else interesting" + } + } + ] + } + }, + 'visible': true, + 'style': { + 'type': 'VECTOR', + 'properties': {} + }, + 'type': 'VECTOR' + } + ], + title: 'my map', +} +const input = { + hideFilterActions: true, + isLayerTOCOpen: false, + openTOCDetails: ['tfi3f', 'edh66'], + mapCenter: { lat: 0.0, lon: 0.0, zoom: 7 } +} +const mapEmbeddable = await factory.createFromState(state, input, parent); + +mapEmbeddable.setLayerList([ + { + 'id': 'gaxya', + 'label': 'My geospatial data', + 'minZoom': 0, + 'maxZoom': 24, + 'alpha': 1, + 'sourceDescriptor': { + 'id': 'b7486', + 'type': 'GEOJSON_FILE', + '__featureCollection': { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [35, 35], [45, 45], [45, 35], [35, 35] + ] + ] + }, + "properties": { + "name": "null island", + "another_prop": "something else interesting" + } + } + ] + } + }, + 'visible': true, + 'style': { + 'type': 'VECTOR', + 'properties': {} + }, + 'type': 'VECTOR' + } +]); +``` diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js index 18c4c78b969744..2c203ffc8fc632 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.js @@ -165,6 +165,11 @@ export class MapEmbeddable extends Embeddable { }); } + async setLayerList(layerList) { + this._layerList = layerList; + return await this._store.dispatch(replaceLayerList(this._layerList)); + } + addFilters = filters => { npStart.plugins.uiActions.executeTriggerActions(APPLY_FILTER_TRIGGER, { embeddable: this, diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts new file mode 100644 index 00000000000000..404909c5c51b88 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -0,0 +1,29 @@ +/* + * 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 './kibana_services'; + +// import the uiExports that we want to "use" +import 'uiExports/inspectorViews'; +import 'uiExports/search'; +import 'uiExports/embeddableFactories'; +import 'uiExports/embeddableActions'; +import 'ui/agg_types'; + +import 'ui/kbn_top_nav'; +import 'ui/autoload/all'; +import 'react-vis/dist/style.css'; + +import './angular/services/gis_map_saved_object_loader'; +import './angular/map_controller'; +import './routes'; +// @ts-ignore +import { PluginInitializerContext } from 'kibana/public'; +import { MapsPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => { + return new MapsPlugin(); +}; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js index f901c8b93e8cd5..65704b4cd83ad5 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.js @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { TooltipSelector } from '../../../components/tooltip_selector'; import { getEMSClient } from '../../../meta'; +import { EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; export class UpdateSourceEditor extends Component { @@ -53,13 +55,29 @@ export class UpdateSourceEditor extends Component { }; render() { - return ( - + + + +
+ +
+
+ + + + +
+ + +
); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index de1b47ea28a910..fbc1703f860037 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -85,7 +85,6 @@ export class ESGeoGridSource extends AbstractESAggSource { metrics={this._descriptor.metrics} renderAs={this._descriptor.requestType} resolution={this._descriptor.resolution} - applyGlobalQuery={this._descriptor.applyGlobalQuery} /> ); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js index cc1e53dc5cb3f8..8a41d477838640 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js @@ -12,8 +12,7 @@ import { indexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; +import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { isMetricCountable } from '../../util/is_metric_countable'; export class UpdateSourceEditor extends Component { @@ -63,11 +62,7 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'resolution', value: e }); }; - _onApplyGlobalQueryChange = applyGlobalQuery => { - this.props.onChange({ propName: 'applyGlobalQuery', value: applyGlobalQuery }); - }; - - _renderMetricsEditor() { + _renderMetricsPanel() { const metricsFilter = this.props.renderAs === RENDER_AS.HEATMAP ? metric => { @@ -77,13 +72,13 @@ export class UpdateSourceEditor extends Component { : null; const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP; return ( -
- + +
- + -
+ ); } render() { return ( - - + {this._renderMetricsPanel()} + - {this._renderMetricsEditor()} + + +
+ +
+
+ + +
+ -
); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js index 2df7dfc3e07641..f0b09f24800841 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { MetricsEditor } from '../../../components/metrics_editor'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; -import { EuiTitle, EuiSpacer } from '@elastic/eui'; +import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; export class UpdateSourceEditor extends Component { state = { @@ -56,30 +55,25 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'metrics', value: metrics }); }; - _onApplyGlobalQueryChange = applyGlobalQuery => { - this.props.onChange({ propName: 'applyGlobalQuery', value: applyGlobalQuery }); - }; - render() { return ( - <> - -
- -
-
+ + + +
+ +
+
+ + +
- - - +
); } } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap index 1e064fdb0dd7d2..85c8d0b354a130 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap @@ -2,344 +2,386 @@ exports[`should enable sort order select when sort field provided 1`] = ` - + + +
+ +
+
+ -
- - + + + - - - - - + - - - - - + + - - - + + + + + + - - + + + +
`; exports[`should render top hits form when useTopHits is true 1`] = ` - + + +
+ +
+
+ -
- - + + + - - - - - + - - - - - - - - + + - - - - - - + + + + + + - - + + + + + + + + + +
`; exports[`should render update source editor 1`] = ` - + + +
+ +
+
+ -
- - + + + - - + - - - - - - - - + + - - - + + + + + + - - + + + +
`; diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 453a1851e47aa8..2a47cb2213be1b 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -85,14 +85,12 @@ export class ESSearchSource extends AbstractESSource { source={this} indexPatternId={this._descriptor.indexPatternId} onChange={onChange} - filterByMapBounds={this._descriptor.filterByMapBounds} tooltipFields={this._tooltipFields} sortField={this._descriptor.sortField} sortOrder={this._descriptor.sortOrder} useTopHits={this._descriptor.useTopHits} topHitsSplitField={this._descriptor.topHitsSplitField} topHitsSize={this._descriptor.topHitsSize} - applyGlobalQuery={this._descriptor.applyGlobalQuery} /> ); } @@ -405,7 +403,7 @@ export class ESSearchSource extends AbstractESSource { searchSource.setField('size', 1); const query = { language: 'kuery', - query: `_id:"${docId}" and _index:${index}` + query: `_id:"${docId}" and _index:"${index}"` }; searchSource.setField('query', query); searchSource.setField('fields', this._getTooltipPropertyNames()); @@ -445,6 +443,10 @@ export class ESSearchSource extends AbstractESSource { return _.get(this._descriptor, 'filterByMapBounds', false); } + isFilterByMapBoundsConfigurable() { + return true; + } + async getLeftJoinFields() { const indexPattern = await this.getIndexPattern(); // Left fields are retrieved from _source. diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js index 1b4e999c29d0a1..b7c332b4c96cbf 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js @@ -9,13 +9,14 @@ import PropTypes from 'prop-types'; import { EuiFormRow, EuiSwitch, - EuiFlexGroup, - EuiFlexItem, EuiSelect, + EuiTitle, + EuiPanel, + EuiSpacer, + EuiHorizontalRule, } from '@elastic/eui'; import { SingleFieldSelect } from '../../../components/single_field_select'; import { TooltipSelector } from '../../../components/tooltip_selector'; -import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; import { indexPatternService } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; @@ -23,12 +24,12 @@ import { getTermsFields, getSourceFields } from '../../../index_pattern_util'; import { ValidatedRange } from '../../../components/validated_range'; import { SORT_ORDER } from '../../../../common/constants'; import { ESDocField } from '../../fields/es_doc_field'; +import { FormattedMessage } from '@kbn/i18n/react'; export class UpdateSourceEditor extends Component { static propTypes = { indexPatternId: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, - filterByMapBounds: PropTypes.bool.isRequired, tooltipFields: PropTypes.arrayOf(PropTypes.object).isRequired, sortField: PropTypes.string, sortOrder: PropTypes.string.isRequired, @@ -94,10 +95,6 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'tooltipProperties', value: propertyNames }); }; - _onFilterByMapBoundsChange = event => { - this.props.onChange({ propName: 'filterByMapBounds', value: event.target.checked }); - }; - onUseTopHitsChange = event => { this.props.onChange({ propName: 'useTopHits', value: event.target.checked }); }; @@ -118,13 +115,27 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'topHitsSize', value: size }); }; - _onApplyGlobalQueryChange = applyGlobalQuery => { - this.props.onChange({ propName: 'applyGlobalQuery', value: applyGlobalQuery }); - }; - renderTopHitsForm() { + const topHitsSwitch = ( + + + + ); + if (!this.props.useTopHits) { - return null; + return topHitsSwitch; } let sizeSlider; @@ -134,7 +145,7 @@ export class UpdateSourceEditor extends Component { label={i18n.translate('xpack.maps.source.esSearch.topHitsSizeLabel', { defaultMessage: 'Documents per entity', })} - display="rowCompressed" + display="columnCompressed" > + {topHitsSwitch} - - - + + +
+ +
+
+ + + + +
+ ); + } + + _renderSortPanel() { + return ( + + +
+ +
+
+ + - - - - - - - - + - - + + {this.renderTopHitsForm()} +
+ ); + } - - - + render() { + return ( + - + {this._renderTooltipsPanel()} + + + {this._renderSortPanel()} + ); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js index e255e2478a37df..a586bc9fb53b20 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/vector_source.js @@ -79,6 +79,10 @@ export class AbstractVectorSource extends AbstractSource { return false; } + isFilterByMapBoundsConfigurable() { + return false; + } + isBoundsAware() { return false; } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js index 6bd8b64ddf8328..b8e4ea0cca4df4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/style_property_legend_row.js @@ -8,7 +8,6 @@ import _ from 'lodash'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { rangeShape } from '../style_option_shapes'; import { getVectorStyleLabel } from '../get_vector_style_label'; import { StyleLegendRow } from '../../../components/style_legend_row'; @@ -25,7 +24,7 @@ export class StylePropertyLegendRow extends Component { } render() { - const { range, style } = this.props; + const { meta: range, style } = this.props; const min = this._formatValue(_.get(range, 'min', EMPTY_VALUE)); const minLabel = this.props.style.isFieldMetaEnabled() && range && range.isMinOutsideStdRange ? `< ${min}` : min; @@ -48,6 +47,6 @@ export class StylePropertyLegendRow extends Component { StylePropertyLegendRow.propTypes = { label: PropTypes.string, fieldFormatter: PropTypes.func, - range: rangeShape, + meta: PropTypes.object, style: PropTypes.object }; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js index 5f4139432a9126..a1c82b29a15906 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js @@ -35,7 +35,7 @@ export class VectorStyleLegend extends Component { const rowDescriptors = rows.map(row => { return { label: row.label, - range: row.range, + range: row.meta, styleOptions: row.style.getOptions(), }; }); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js index a2edc8cb4f686f..d2b5178174e125 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js @@ -35,8 +35,3 @@ export const dynamicSizeShape = PropTypes.shape({ maxSize: PropTypes.number.isRequired, field: fieldShape, }); - -export const rangeShape = PropTypes.shape({ - min: PropTypes.number.isRequired, - max: PropTypes.number.isRequired, -}); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js index a72502f9f17fba..1ce94f796573bc 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js @@ -60,4 +60,51 @@ export class DynamicStyleProperty extends AbstractStyleProperty { getFieldMetaOptions() { return _.get(this.getOptions(), 'fieldMetaOptions', {}); } + + + pluckStyleMetaFromFeatures(features) { + + const name = this.getField().getName(); + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < features.length; i++) { + const feature = features[i]; + const newValue = parseFloat(feature.properties[name]); + if (!isNaN(newValue)) { + min = Math.min(min, newValue); + max = Math.max(max, newValue); + } + } + + return (min === Infinity || max === -Infinity) ? null : ({ + min: min, + max: max, + delta: max - min + }); + + } + + pluckStyleMetaFromFieldMetaData(fieldMetaData) { + + const realFieldName = this._field.getESDocFieldName ? this._field.getESDocFieldName() : this._field.getName(); + const stats = fieldMetaData[realFieldName]; + if (!stats) { + return null; + } + + const sigma = _.get(this.getFieldMetaOptions(), 'sigma', DEFAULT_SIGMA); + const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); + const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); + const min = Math.max(stats.min, stdLowerBounds); + const max = Math.min(stats.max, stdUpperBounds); + return { + min, + max, + delta: max - min, + isMinOutsideStdRange: stats.min < stdLowerBounds, + isMaxOutsideStdRange: stats.max > stdUpperBounds, + }; + + } + } diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js index 426af96b63ba24..3b7857180ccae6 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/vector_style.js @@ -180,24 +180,17 @@ export class VectorStyle extends AbstractStyle { } async pluckStyleMetaFromSourceDataRequest(sourceDataRequest) { + const features = _.get(sourceDataRequest.getData(), 'features', []); if (features.length === 0) { return {}; } - const scaledFields = this.getDynamicPropertiesArray() - .map(styleProperty => { - return { - name: styleProperty.getField().getName(), - min: Infinity, - max: -Infinity - }; - }); + const dynamicProperties = this.getDynamicPropertiesArray(); const supportedFeatures = await this._source.getSupportedShapeTypes(); const isSingleFeatureType = supportedFeatures.length === 1; - - if (scaledFields.length === 0 && isSingleFeatureType) { + if (dynamicProperties.length === 0 && isSingleFeatureType) { // no meta data to pull from source data request. return {}; } @@ -216,15 +209,6 @@ export class VectorStyle extends AbstractStyle { if (!hasPolygons && POLYGONS.includes(feature.geometry.type)) { hasPolygons = true; } - - for (let j = 0; j < scaledFields.length; j++) { - const scaledField = scaledFields[j]; - const newValue = parseFloat(feature.properties[scaledField.name]); - if (!isNaN(newValue)) { - scaledField.min = Math.min(scaledField.min, newValue); - scaledField.max = Math.max(scaledField.max, newValue); - } - } } const featuresMeta = { @@ -235,13 +219,11 @@ export class VectorStyle extends AbstractStyle { } }; - scaledFields.forEach(({ min, max, name }) => { - if (min !== Infinity && max !== -Infinity) { - featuresMeta[name] = { - min, - max, - delta: max - min, - }; + dynamicProperties.forEach(dynamicProperty => { + const styleMeta = dynamicProperty.pluckStyleMetaFromFeatures(features); + if (styleMeta) { + const name = dynamicProperty.getField().getName(); + featuresMeta[name] = styleMeta; } }); @@ -290,13 +272,15 @@ export class VectorStyle extends AbstractStyle { return this._isOnlySingleFeatureType(VECTOR_SHAPE_TYPES.POLYGON); } - _getFieldRange = (fieldName) => { - const fieldRangeFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + _getFieldMeta = (fieldName) => { + + const fieldMetaFromLocalFeatures = _.get(this._descriptor, ['__styleMeta', fieldName]); + const dynamicProps = this.getDynamicPropertiesArray(); const dynamicProp = dynamicProps.find(dynamicProp => { return fieldName === dynamicProp.getField().getName(); }); if (!dynamicProp || !dynamicProp.isFieldMetaEnabled()) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } let dataRequestId; @@ -313,34 +297,19 @@ export class VectorStyle extends AbstractStyle { } if (!dataRequestId) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } const styleMetaDataRequest = this._layer._findDataRequestForSource(dataRequestId); if (!styleMetaDataRequest || !styleMetaDataRequest.hasData()) { - return fieldRangeFromLocalFeatures; + return fieldMetaFromLocalFeatures; } const data = styleMetaDataRequest.getData(); - const field = dynamicProp.getField(); - const realFieldName = field.getESDocFieldName ? field.getESDocFieldName() : field.getName(); - const stats = data[realFieldName]; - if (!stats) { - return fieldRangeFromLocalFeatures; - } + const fieldMeta = dynamicProp.pluckStyleMetaFromFieldMetaData(data); + + return fieldMeta ? fieldMeta : fieldMetaFromLocalFeatures; - const sigma = _.get(dynamicProp.getFieldMetaOptions(), 'sigma', 3); - const stdLowerBounds = stats.avg - (stats.std_deviation * sigma); - const stdUpperBounds = stats.avg + (stats.std_deviation * sigma); - const min = Math.max(stats.min, stdLowerBounds); - const max = Math.min(stats.max, stdUpperBounds); - return { - min, - max, - delta: max - min, - isMinOutsideStdRange: stats.min < stdLowerBounds, - isMaxOutsideStdRange: stats.max > stdUpperBounds, - }; } _getStyleMeta = () => { @@ -392,7 +361,7 @@ export class VectorStyle extends AbstractStyle { return { label: await style.getField().getLabel(), fieldFormatter: await this._source.getFieldFormatter(style.getField().getName()), - range: this._getFieldRange(style.getField().getName()), + meta: this._getFieldMeta(style.getField().getName()), style, }; }); @@ -402,7 +371,7 @@ export class VectorStyle extends AbstractStyle { return ; } - _getStyleFields() { + _getFeatureStyleParams() { return this.getDynamicPropertiesArray() .map(styleProperty => { @@ -424,7 +393,7 @@ export class VectorStyle extends AbstractStyle { supportsFeatureState, isScaled, name: field.getName(), - range: this._getFieldRange(field.getName()), + meta: this._getFieldMeta(field.getName()), computedName: getComputedFieldName(styleProperty.getStyleName(), field.getName()), }; }); @@ -443,14 +412,14 @@ export class VectorStyle extends AbstractStyle { } } - setFeatureState(featureCollection, mbMap, sourceId) { + setFeatureStateAndStyleProps(featureCollection, mbMap, mbSourceId) { if (!featureCollection) { return; } - const styleFields = this._getStyleFields(); - if (styleFields.length === 0) { + const featureStateParams = this._getFeatureStyleParams(); + if (featureStateParams.length === 0) { return; } @@ -464,8 +433,8 @@ export class VectorStyle extends AbstractStyle { for (let i = 0; i < featureCollection.features.length; i++) { const feature = featureCollection.features[i]; - for (let j = 0; j < styleFields.length; j++) { - const { supportsFeatureState, isScaled, name, range, computedName } = styleFields[j]; + for (let j = 0; j < featureStateParams.length; j++) { + const { supportsFeatureState, isScaled, name, meta: range, computedName } = featureStateParams[j]; const value = parseFloat(feature.properties[name]); let styleValue; if (isScaled) { @@ -484,15 +453,16 @@ export class VectorStyle extends AbstractStyle { feature.properties[computedName] = styleValue; } } - tmpFeatureIdentifier.source = sourceId; + tmpFeatureIdentifier.source = mbSourceId; tmpFeatureIdentifier.id = feature.id; mbMap.setFeatureState(tmpFeatureIdentifier, tmpFeatureState); } - const hasGeoJsonProperties = styleFields.some(({ supportsFeatureState }) => { - return !supportsFeatureState; - }); - return hasGeoJsonProperties; + //returns boolean indicating if styles do not support feature-state and some values are stored in geojson properties + //this return-value is used in an optimization for style-updates with mapbox-gl. + //`true` indicates the entire data needs to reset on the source (otherwise the style-rules will not be reapplied) + //`false` indicates the data does not need to be reset on the store, because styles are re-evaluated if they use featureState + return featureStateParams.some(({ supportsFeatureState }) => !supportsFeatureState); } arePointsSymbolizedAsCircles() { diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 60a6201ac872cb..f7db17d06ee2a4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -509,7 +509,7 @@ export class VectorLayer extends AbstractLayer { // "feature-state" data expressions are not supported with layout properties. // To work around this limitation, // scaled layout properties (like icon-size) must fall back to geojson property values :( - const hasGeoJsonProperties = this._style.setFeatureState(featureCollection, mbMap, this.getId()); + const hasGeoJsonProperties = this._style.setFeatureStateAndStyleProps(featureCollection, mbMap, this.getId()); if (featureCollection !== featureCollectionOnMap || hasGeoJsonProperties) { mbGeoJSONSource.setData(featureCollection); } diff --git a/x-pack/legacy/plugins/maps/public/legacy.ts b/x-pack/legacy/plugins/maps/public/legacy.ts new file mode 100644 index 00000000000000..684d7b16fbb3b1 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/legacy.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. + */ + +import { npSetup, npStart } from 'ui/new_platform'; +// @ts-ignore Untyped Module +import { uiModules } from 'ui/modules'; +import { PluginInitializerContext } from 'kibana/public'; // eslint-disable-line import/order +import { plugin } from '.'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +const setupPlugins = { + __LEGACY: { + uiModules, + }, + plugins: npSetup.plugins, +}; + +const startPlugins = { + plugins: npStart.plugins, +}; + +export const setup = pluginInstance.setup(npSetup.core, setupPlugins); +export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/x-pack/legacy/plugins/maps/public/plugin.ts b/x-pack/legacy/plugins/maps/public/plugin.ts new file mode 100644 index 00000000000000..4e6d52d20db645 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/plugin.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreStart } from 'src/core/public'; +// @ts-ignore +import { wrapInI18nContext } from 'ui/i18n'; +// @ts-ignore +import { MapListing } from './components/map_listing'; + +/** + * These are the interfaces with your public contracts. You should export these + * for other plugins to use in _their_ `SetupDeps`/`StartDeps` interfaces. + * @public + */ +export type MapsPluginSetup = ReturnType; +export type MapsPluginStart = ReturnType; + +/** @internal */ +export class MapsPlugin implements Plugin { + public setup(core: any, plugins: any) { + const app = plugins.__LEGACY.uiModules.get('app/maps', ['ngRoute', 'react']); + app.directive('mapListing', function(reactDirective: any) { + return reactDirective(wrapInI18nContext(MapListing)); + }); + } + + public start(core: CoreStart, plugins: any) {} +} diff --git a/x-pack/legacy/plugins/maps/public/index.js b/x-pack/legacy/plugins/maps/public/routes.js similarity index 79% rename from x-pack/legacy/plugins/maps/public/index.js rename to x-pack/legacy/plugins/maps/public/routes.js index 964753f464d95e..ce8ae1359d3b6e 100644 --- a/x-pack/legacy/plugins/maps/public/index.js +++ b/x-pack/legacy/plugins/maps/public/routes.js @@ -4,40 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import './kibana_services'; - -import { wrapInI18nContext } from 'ui/i18n'; import { i18n } from '@kbn/i18n'; - -// import the uiExports that we want to "use" -import 'uiExports/inspectorViews'; -import 'uiExports/search'; -import 'uiExports/embeddableFactories'; -import 'uiExports/embeddableActions'; -import 'ui/agg_types'; - import { capabilities } from 'ui/capabilities'; import chrome from 'ui/chrome'; import routes from 'ui/routes'; -import 'ui/kbn_top_nav'; -import { uiModules } from 'ui/modules'; import { docTitle } from 'ui/doc_title'; -import 'ui/autoload/all'; -import 'react-vis/dist/style.css'; - -import './angular/services/gis_map_saved_object_loader'; -import './angular/map_controller'; import listingTemplate from './angular/listing_ng_wrapper.html'; import mapTemplate from './angular/map.html'; -import { MapListing } from './components/map_listing'; import { npStart } from 'ui/new_platform'; -const app = uiModules.get('app/maps', ['ngRoute', 'react']); - -app.directive('mapListing', function (reactDirective) { - return reactDirective(wrapInI18nContext(MapListing)); -}); - routes.enable(); routes diff --git a/x-pack/legacy/plugins/graph/server/index.js b/x-pack/legacy/plugins/ml/common/constants/app.ts similarity index 83% rename from x-pack/legacy/plugins/graph/server/index.js rename to x-pack/legacy/plugins/ml/common/constants/app.ts index be72e92176554e..140a709b0c42be 100644 --- a/x-pack/legacy/plugins/graph/server/index.js +++ b/x-pack/legacy/plugins/ml/common/constants/app.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initServer } from './init_server'; +export const API_BASE_PATH = '/api/transform/'; diff --git a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts index 96a46c92cb602e..48e88e79f96740 100644 --- a/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts +++ b/x-pack/legacy/plugins/ml/common/constants/feature_flags.ts @@ -9,6 +9,5 @@ // indices and aliases exist. Based on that the final setting will be available // as an injectedVar on the client side and can be accessed like: // -// import chrome from 'ui/chrome'; -// const mlAnnotationsEnabled = chrome.getInjected('mlAnnotationsEnabled', false); + export const FEATURE_ANNOTATIONS_ENABLED = true; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts b/x-pack/legacy/plugins/ml/common/constants/new_job.ts similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/constants.ts rename to x-pack/legacy/plugins/ml/common/constants/new_job.ts diff --git a/x-pack/legacy/plugins/ml/common/types/kibana.ts b/x-pack/legacy/plugins/ml/common/types/kibana.ts index 86db2ce59d7e78..d647bd882162b7 100644 --- a/x-pack/legacy/plugins/ml/common/types/kibana.ts +++ b/x-pack/legacy/plugins/ml/common/types/kibana.ts @@ -6,6 +6,8 @@ // custom edits or fixes for default kibana types which are incomplete +import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; + export type IndexPatternTitle = string; export type callWithRequestType = (action: string, params?: any) => Promise; @@ -14,3 +16,12 @@ export interface Route { id: string; k7Breadcrumbs: () => any; } + +export type IndexPatternSavedObject = SimpleSavedObject; +export type SavedSearchSavedObject = SimpleSavedObject; + +export function isSavedSearchSavedObject( + ss: SavedSearchSavedObject | null +): ss is SavedSearchSavedObject { + return ss !== null; +} diff --git a/x-pack/legacy/plugins/ml/common/types/modules.ts b/x-pack/legacy/plugins/ml/common/types/modules.ts index 52259d8748a95b..cd6395500a804a 100644 --- a/x-pack/legacy/plugins/ml/common/types/modules.ts +++ b/x-pack/legacy/plugins/ml/common/types/modules.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectAttributes } from 'src/core/server/types'; +import { SavedObjectAttributes } from 'src/core/public'; import { Datafeed, Job } from '../../public/application/jobs/new_job/common/job_creator/configs'; export interface ModuleJob { diff --git a/x-pack/legacy/plugins/ml/common/util/job_utils.js b/x-pack/legacy/plugins/ml/common/util/job_utils.js index 999eb44b372bcd..cef3475a9654f0 100644 --- a/x-pack/legacy/plugins/ml/common/util/job_utils.js +++ b/x-pack/legacy/plugins/ml/common/util/job_utils.js @@ -12,7 +12,7 @@ import numeral from '@elastic/numeral'; import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation'; import { parseInterval } from './parse_interval'; import { maxLengthValidator } from './validators'; -import { CREATED_BY_LABEL } from '../../public/application/jobs/new_job/common/job_creator/util/constants'; +import { CREATED_BY_LABEL } from '../../common/constants/new_job'; // work out the default frequency based on the bucket_span in seconds export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds) { diff --git a/x-pack/legacy/plugins/ml/index.ts b/x-pack/legacy/plugins/ml/index.ts index 9b42998c814fd7..3078a0c812ff1b 100755 --- a/x-pack/legacy/plugins/ml/index.ts +++ b/x-pack/legacy/plugins/ml/index.ts @@ -41,9 +41,9 @@ export const ml = (kibana: any) => { }), icon: 'plugins/ml/application/ml.svg', euiIconType: 'machineLearningApp', - main: 'plugins/ml/application/app', + main: 'plugins/ml/legacy', }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), + styleSheetPaths: resolve(__dirname, 'public/application/index.scss'), hacks: ['plugins/ml/application/hacks/toggle_app_link_in_nav'], savedObjectSchemas: { 'ml-telemetry': { diff --git a/x-pack/legacy/plugins/ml/kibana.json b/x-pack/legacy/plugins/ml/kibana.json new file mode 100644 index 00000000000000..f36b4848186906 --- /dev/null +++ b/x-pack/legacy/plugins/ml/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "ml", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["ml"], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx index 883754896487e8..7e2d651439ae30 100644 --- a/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx +++ b/x-pack/legacy/plugins/ml/public/application/access_denied/index.tsx @@ -4,36 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -import { AccessDeniedPage } from './page'; - -const module = uiModules.get('apps/ml', ['react']); - -const template = ``; - -uiRoutes.when('/access-denied', { - template, -}); - -module.directive('accessDenied', function() { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - ReactDOM.render( - {React.createElement(AccessDeniedPage)}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx index 1c908e114cbebc..32b2ade5dc9dce 100644 --- a/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/access_denied/page.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { NavigationMenu } from '../components/navigation_menu'; -export const AccessDeniedPage = () => ( +export const Page = () => ( diff --git a/x-pack/legacy/plugins/ml/public/application/app.js b/x-pack/legacy/plugins/ml/public/application/app.js deleted file mode 100644 index 722e2c8d05e9b9..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/app.js +++ /dev/null @@ -1,36 +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 'uiExports/savedObjectTypes'; - -import 'ui/autoload/all'; - -// needed to make syntax highlighting work in ace editors -import 'ace'; - -import './access_denied'; -import './jobs'; -import './overview'; -import './services/calendar_service'; -import './data_frame_analytics'; -import './datavisualizer'; -import './explorer'; -import './timeseriesexplorer'; -import './components/navigation_menu'; -import './components/loading_indicator'; -import './settings'; - -import uiRoutes from 'ui/routes'; - -if (typeof uiRoutes.enable === 'function') { - uiRoutes.enable(); -} - -uiRoutes - .otherwise({ - redirectTo: '/overview' - }); diff --git a/x-pack/legacy/plugins/ml/public/application/app.tsx b/x-pack/legacy/plugins/ml/public/application/app.tsx new file mode 100644 index 00000000000000..085e395f2ebf71 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/app.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import ReactDOM from 'react-dom'; + +import 'uiExports/savedObjectTypes'; + +import 'ui/autoload/all'; + +// needed to make syntax highlighting work in ace editors +import 'ace'; +import { AppMountParameters, CoreStart } from 'kibana/public'; +import { + IndexPatternsContract, + Plugin as DataPlugin, +} from '../../../../../../src/plugins/data/public'; + +import { KibanaConfigTypeFix } from './contexts/kibana'; + +import { MlRouter } from './routing'; + +export interface MlDependencies extends AppMountParameters { + npData: ReturnType; + indexPatterns: IndexPatternsContract; +} + +interface AppProps { + coreStart: CoreStart; + indexPatterns: IndexPatternsContract; +} + +const App: FC = ({ coreStart, indexPatterns }) => { + const config = (coreStart.uiSettings as never) as KibanaConfigTypeFix; // TODO - make this UiSettingsClientContract, get rid of KibanaConfigTypeFix + + return ( + + ); +}; + +export const renderApp = ( + coreStart: CoreStart, + depsStart: object, + { element, indexPatterns }: MlDependencies +) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 909abfd4abc233..640ae8f962eed6 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/legacy/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -35,7 +35,6 @@ import { } from '@elastic/eui/lib/services'; import { formatDate } from '@elastic/eui/lib/services/format'; -import chrome from 'ui/chrome'; import { addItemToRecentlyAccessed } from '../../../util/recently_accessed'; import { ml } from '../../../services/ml_api_service'; @@ -206,7 +205,7 @@ const AnnotationsTable = injectI18n(class AnnotationsTable extends Component { const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); + window.open(`#/timeseriesexplorer${url}`, '_self'); } onMouseOverRow = (record) => { diff --git a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js index 19cd77655f97c4..f237bcc2efc53f 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/legacy/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -205,7 +205,7 @@ export const LinksMenu = injectI18n(class LinksMenu extends Component { }); // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = `${chrome.getBasePath()}/app/ml#/timeseriesexplorer`; + let path = '#/timeseriesexplorer'; path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; window.open(path, '_blank'); } diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss new file mode 100644 index 00000000000000..b164e605a24884 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_color_range_legend.scss @@ -0,0 +1,18 @@ +/* Overrides for d3/svg default styles */ +.mlColorRangeLegend { + text { + @include fontSize($euiFontSizeXS - 2px); + fill: $euiColorDarkShade; + } + + .axis path { + fill: none; + stroke: none; + } + + .axis line { + fill: none; + stroke: $euiColorMediumShade; + shape-rendering: crispEdges; + } +} diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss new file mode 100644 index 00000000000000..c7cd3faac0dcf1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/_index.scss @@ -0,0 +1 @@ +@import 'color_range_legend'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx new file mode 100644 index 00000000000000..d6f0d347a57ec4 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/color_range_legend.tsx @@ -0,0 +1,153 @@ +/* + * 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, useRef, FC } from 'react'; +import d3 from 'd3'; + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +const COLOR_RANGE_RESOLUTION = 10; + +interface ColorRangeLegendProps { + colorRange: (d: number) => string; + justifyTicks?: boolean; + showTicks?: boolean; + title?: string; + width?: number; +} + +/** + * Component to render a legend for color ranges to be used for color coding + * table cells and visualizations. + * + * This current version supports normalized value ranges (0-1) only. + * + * @param props ColorRangeLegendProps + */ +export const ColorRangeLegend: FC = ({ + colorRange, + justifyTicks = false, + showTicks = true, + title, + width = 250, +}) => { + const d3Container = useRef(null); + + const scale = d3.range(COLOR_RANGE_RESOLUTION + 1).map(d => ({ + offset: (d / COLOR_RANGE_RESOLUTION) * 100, + stopColor: colorRange(d / COLOR_RANGE_RESOLUTION), + })); + + useEffect(() => { + if (d3Container.current === null) { + return; + } + + const wrapperHeight = 32; + const wrapperWidth = width; + + // top: 2 — adjust vertical alignment with title text + // bottom: 20 — room for axis ticks and labels + // left/right: 1 — room for first and last axis tick + // when justifyTicks is enabled, the left margin is increased to not cut off the first tick label + const margin = { top: 2, bottom: 20, left: justifyTicks || !showTicks ? 1 : 4, right: 1 }; + + const legendWidth = wrapperWidth - margin.left - margin.right; + const legendHeight = wrapperHeight - margin.top - margin.bottom; + + // remove, then redraw the legend + d3.select(d3Container.current) + .selectAll('*') + .remove(); + + const wrapper = d3 + .select(d3Container.current) + .classed('mlColorRangeLegend', true) + .attr('width', wrapperWidth) + .attr('height', wrapperHeight) + .append('g') + .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); + + // append gradient bar + const gradient = wrapper + .append('defs') + .append('linearGradient') + .attr('id', 'mlColorRangeGradient') + .attr('x1', '0%') + .attr('y1', '0%') + .attr('x2', '100%') + .attr('y2', '0%') + .attr('spreadMethod', 'pad'); + + scale.forEach(function(d) { + gradient + .append('stop') + .attr('offset', `${d.offset}%`) + .attr('stop-color', d.stopColor) + .attr('stop-opacity', 1); + }); + + wrapper + .append('rect') + .attr('x1', 0) + .attr('y1', 0) + .attr('width', legendWidth) + .attr('height', legendHeight) + .style('fill', 'url(#mlColorRangeGradient)'); + + const axisScale = d3.scale + .linear() + .domain([0, 1]) + .range([0, legendWidth]); + + // Using this formatter ensures we get e.g. `0` and not `0.0`, but still `0.1`, `0.2` etc. + const tickFormat = d3.format(''); + const legendAxis = d3.svg + .axis() + .scale(axisScale) + .orient('bottom') + .tickFormat(tickFormat) + .tickSize(legendHeight + 4) + .ticks(legendWidth / 40); + + wrapper + .append('g') + .attr('class', 'legend axis') + .attr('transform', 'translate(0, 0)') + .call(legendAxis); + + // Adjust the alignment of the first and last tick text + // so that the tick labels don't overflow the color range. + if (justifyTicks || !showTicks) { + const text = wrapper.selectAll('text')[0]; + if (text.length > 1) { + d3.select(text[0]).style('text-anchor', 'start'); + d3.select(text[text.length - 1]).style('text-anchor', 'end'); + } + } + + if (!showTicks) { + wrapper.selectAll('.axis line').style('display', 'none'); + } + }, [JSON.stringify(scale), d3Container.current]); + + if (title === undefined) { + return ; + } + + return ( + + + + {title} + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/graph/server/lib/index.js b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts similarity index 57% rename from x-pack/legacy/plugins/graph/server/lib/index.js rename to x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts index 6fe112004afbc9..93a1ec40f1d5e7 100644 --- a/x-pack/legacy/plugins/graph/server/lib/index.js +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/index.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; - -export { - callEsGraphExploreApi, - callEsSearchApi, -} from './es'; - +export { ColorRangeLegend } from './color_range_legend'; export { - getCallClusterPre, - verifyApiAccessPre, -} from './pre'; + colorRangeOptions, + colorRangeScaleOptions, + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from './use_color_range'; diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts new file mode 100644 index 00000000000000..f047ae800266b7 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { influencerColorScaleFactory } from './use_color_range'; + +jest.mock('../../contexts/ui/use_ui_chrome_context'); + +describe('useColorRange', () => { + test('influencerColorScaleFactory(1)', () => { + const influencerColorScale = influencerColorScaleFactory(1); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0.1); + expect(influencerColorScale(0.2)).toBe(0.2); + expect(influencerColorScale(0.3)).toBe(0.3); + expect(influencerColorScale(0.4)).toBe(0.4); + expect(influencerColorScale(0.5)).toBe(0.5); + expect(influencerColorScale(0.6)).toBe(0.6); + expect(influencerColorScale(0.7)).toBe(0.7); + expect(influencerColorScale(0.8)).toBe(0.8); + expect(influencerColorScale(0.9)).toBe(0.9); + expect(influencerColorScale(1)).toBe(1); + }); + + test('influencerColorScaleFactory(2)', () => { + const influencerColorScale = influencerColorScaleFactory(2); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0); + expect(influencerColorScale(0.2)).toBe(0); + expect(influencerColorScale(0.3)).toBe(0); + expect(influencerColorScale(0.4)).toBe(0); + expect(influencerColorScale(0.5)).toBe(0); + expect(influencerColorScale(0.6)).toBe(0.04999999999999999); + expect(influencerColorScale(0.7)).toBe(0.09999999999999998); + expect(influencerColorScale(0.8)).toBe(0.15000000000000002); + expect(influencerColorScale(0.9)).toBe(0.2); + expect(influencerColorScale(1)).toBe(0.25); + }); + + test('influencerColorScaleFactory(3)', () => { + const influencerColorScale = influencerColorScaleFactory(3); + + expect(influencerColorScale(0)).toBe(0); + expect(influencerColorScale(0.1)).toBe(0); + expect(influencerColorScale(0.2)).toBe(0); + expect(influencerColorScale(0.3)).toBe(0); + expect(influencerColorScale(0.4)).toBe(0.05000000000000003); + expect(influencerColorScale(0.5)).toBe(0.125); + expect(influencerColorScale(0.6)).toBe(0.2); + expect(influencerColorScale(0.7)).toBe(0.27499999999999997); + expect(influencerColorScale(0.8)).toBe(0.35000000000000003); + expect(influencerColorScale(0.9)).toBe(0.425); + expect(influencerColorScale(1)).toBe(0.5); + }); +}); diff --git a/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts new file mode 100644 index 00000000000000..f9c5e6ff81f9ea --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -0,0 +1,188 @@ +/* + * 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 d3 from 'd3'; + +import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; + +import { i18n } from '@kbn/i18n'; + +import { useUiChromeContext } from '../../contexts/ui/use_ui_chrome_context'; + +/** + * Custom color scale factory that takes the amount of feature influencers + * into account to adjust the contrast of the color range. This is used for + * color coding for outlier detection where the amount of feature influencers + * affects the threshold from which the influencers value can actually be + * considered influential. + * + * @param n number of influencers + * @returns a function suitable as a preprocessor for d3.scale.linear() + */ +export const influencerColorScaleFactory = (n: number) => (t: number) => { + // for 1 influencer or less we fall back to a plain linear scale. + if (n <= 1) { + return t; + } + + if (t < 1 / n) { + return 0; + } + if (t < 3 / n) { + return (n / 4) * (t - 1 / n); + } + return 0.5 + (t - 3 / n); +}; + +export enum COLOR_RANGE_SCALE { + LINEAR = 'linear', + INFLUENCER = 'influencer', + SQRT = 'sqrt', +} + +/** + * Color range scale options in the format for EuiSelect's options prop. + */ +export const colorRangeScaleOptions = [ + { + value: COLOR_RANGE_SCALE.LINEAR, + text: i18n.translate('xpack.ml.components.colorRangeLegend.linearScaleLabel', { + defaultMessage: 'Linear', + }), + }, + { + value: COLOR_RANGE_SCALE.INFLUENCER, + text: i18n.translate('xpack.ml.components.colorRangeLegend.influencerScaleLabel', { + defaultMessage: 'Influencer custom scale', + }), + }, + { + value: COLOR_RANGE_SCALE.SQRT, + text: i18n.translate('xpack.ml.components.colorRangeLegend.sqrtScaleLabel', { + defaultMessage: 'Sqrt', + }), + }, +]; + +export enum COLOR_RANGE { + BLUE = 'blue', + RED = 'red', + RED_GREEN = 'red-green', + GREEN_RED = 'green-red', + YELLOW_GREEN_BLUE = 'yellow-green-blue', +} + +/** + * Color range options in the format for EuiSelect's options prop. + */ +export const colorRangeOptions = [ + { + value: COLOR_RANGE.BLUE, + text: i18n.translate('xpack.ml.components.colorRangeLegend.blueColorRangeLabel', { + defaultMessage: 'Blue', + }), + }, + { + value: COLOR_RANGE.RED, + text: i18n.translate('xpack.ml.components.colorRangeLegend.redColorRangeLabel', { + defaultMessage: 'Red', + }), + }, + { + value: COLOR_RANGE.RED_GREEN, + text: i18n.translate('xpack.ml.components.colorRangeLegend.redGreenColorRangeLabel', { + defaultMessage: 'Red - Green', + }), + }, + { + value: COLOR_RANGE.GREEN_RED, + text: i18n.translate('xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel', { + defaultMessage: 'Green - Red', + }), + }, + { + value: COLOR_RANGE.YELLOW_GREEN_BLUE, + text: i18n.translate('xpack.ml.components.colorRangeLegend.yellowGreenBlueColorRangeLabel', { + defaultMessage: 'Yellow - Green - Blue', + }), + }, +]; + +/** + * A custom Yellow-Green-Blue color range to demonstrate the support + * for more complex ranges with more than two colors. + */ +const coloursYGB = [ + '#FFFFDD', + '#AAF191', + '#80D385', + '#61B385', + '#3E9583', + '#217681', + '#285285', + '#1F2D86', + '#000086', +]; +const colourRangeYGB = d3.range(0, 1, 1.0 / (coloursYGB.length - 1)); +colourRangeYGB.push(1); + +const colorDomains = { + [COLOR_RANGE.BLUE]: [0, 1], + [COLOR_RANGE.RED]: [0, 1], + [COLOR_RANGE.RED_GREEN]: [0, 1], + [COLOR_RANGE.GREEN_RED]: [0, 1], + [COLOR_RANGE.YELLOW_GREEN_BLUE]: colourRangeYGB, +}; + +/** + * Custom hook to get a d3 based color range to be used for color coding in table cells. + * + * @param colorRange COLOR_RANGE enum. + * @param colorRangeScale COLOR_RANGE_SCALE enum. + * @param featureCount + */ +export const useColorRange = ( + colorRange = COLOR_RANGE.BLUE, + colorRangeScale = COLOR_RANGE_SCALE.LINEAR, + featureCount = 1 +) => { + const euiTheme = useUiChromeContext() + .getUiSettingsClient() + .get('theme:darkMode') + ? euiThemeDark + : euiThemeLight; + + const colorRanges = { + [COLOR_RANGE.BLUE]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)], + [COLOR_RANGE.RED]: [d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorDanger)], + [COLOR_RANGE.RED_GREEN]: ['red', 'green'], + [COLOR_RANGE.GREEN_RED]: ['green', 'red'], + [COLOR_RANGE.YELLOW_GREEN_BLUE]: coloursYGB, + }; + + const linearScale = d3.scale + .linear() + .domain(colorDomains[colorRange]) + // typings for .range() incorrectly don't allow passing in a color extent. + // @ts-ignore + .range(colorRanges[colorRange]); + const influencerColorScale = influencerColorScaleFactory(featureCount); + const influencerScaleLinearWrapper = (n: number) => linearScale(influencerColorScale(n)); + + const scaleTypes = { + [COLOR_RANGE_SCALE.LINEAR]: linearScale, + [COLOR_RANGE_SCALE.INFLUENCER]: influencerScaleLinearWrapper, + [COLOR_RANGE_SCALE.SQRT]: d3.scale + .sqrt() + .domain(colorDomains[colorRange]) + // typings for .range() incorrectly don't allow passing in a color extent. + // @ts-ignore + .range(colorRanges[colorRange]), + }; + + return scaleTypes[colorRangeScale]; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts index e7d191a31e034e..94a502e6eadde8 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/data_recognizer.d.ts @@ -7,11 +7,11 @@ import { FC } from 'react'; import { IndexPattern } from 'ui/index_patterns'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; declare const DataRecognizer: FC<{ indexPattern: IndexPattern; - savedSearch?: SavedSearch; + savedSearch?: SavedSearchSavedObject | null; results: { count: number; onChange?: Function; diff --git a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js index 6f511abf89e310..79b1b501c3829f 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/legacy/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -20,7 +20,7 @@ export const RecognizedResult = ({ indexPattern, savedSearch }) => { - const id = (savedSearch === undefined || savedSearch.id === undefined) ? + const id = (savedSearch === null) ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`; diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index cff174eb5627fe..5735faa9c6f52c 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -7,7 +7,6 @@ import React, { FC, useState } from 'react'; import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { TabId } from './navigation_menu'; export interface Tab { @@ -82,7 +81,7 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { return ( diff --git a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx index 7014164ad97561..20fa2cca41231a 100644 --- a/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx +++ b/x-pack/legacy/plugins/ml/public/application/components/navigation_menu/tabs.tsx @@ -7,7 +7,6 @@ import React, { FC, useState } from 'react'; import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { Tab } from './main_tabs'; import { TabId } from './navigation_menu'; @@ -84,7 +83,7 @@ export const Tabs: FC = ({ tabId, mainTabId, disableLinks }) => { data-test-subj={ TAB_TEST_SUBJECT[id as TAB_TEST_SUBJECTS] + (id === selectedTabId ? ' selected' : '') } - href={`${chrome.getBasePath()}/app/ml#/${id}`} + href={`#/${id}`} key={`${id}-key`} color="text" > diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts index 2bff760ed3711d..cbbdaf410445e0 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/__mocks__/saved_search.ts @@ -4,14 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { searchSourceMock } from '../../../../../../../../../src/legacy/ui/public/courier/search_source/mocks'; -import { SearchSourceContract } from '../../../../../../../../../src/legacy/ui/public/courier'; - -export const savedSearchMock = { +export const savedSearchMock: any = { id: 'the-saved-search-id', - title: 'the-saved-search-title', - searchSource: searchSourceMock as SearchSourceContract, - columns: [], - sort: [], - destroy: () => {}, + type: 'search', + attributes: { + title: 'the-saved-search-title', + kibanaSavedObjectMeta: { + searchSourceJSON: + '{"highlightAll":true,"version":true,"query":{"query":"foo : \\"bar\\" ","language":"kuery"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}', + }, + }, + references: [ + { + name: 'kibanaSavedObjectMeta.searchSourceJSON.index', + type: 'index-pattern', + id: 'the-index-pattern-id', + }, + ], + migrationVersion: { search: '7.5.0' }, + error: undefined, }; diff --git a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 00989245e20e7f..9d0a3bc43e2582 100644 --- a/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/legacy/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -7,12 +7,11 @@ import React from 'react'; import { KibanaConfig } from 'src/legacy/server/kbn_server'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; - import { IndexPattern, IndexPatternsContract, } from '../../../../../../../../src/plugins/data/public'; +import { SavedSearchSavedObject } from '../../../../common/types/kibana'; // set() method is missing in original d.ts export interface KibanaConfigTypeFix extends KibanaConfig { @@ -21,8 +20,8 @@ export interface KibanaConfigTypeFix extends KibanaConfig { export interface KibanaContextValue { combinedQuery: any; - currentIndexPattern: IndexPattern; - currentSavedSearch: SavedSearch; + currentIndexPattern: IndexPattern; // TODO this should be IndexPattern or null + currentSavedSearch: SavedSearchSavedObject | null; indexPatterns: IndexPatternsContract; kibanaConfig: KibanaConfigTypeFix; } diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss index c231c405b5369b..962d3f4c7bd541 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/_index.scss @@ -1,5 +1,6 @@ @import 'pages/analytics_exploration/components/exploration/index'; @import 'pages/analytics_exploration/components/regression_exploration/index'; +@import 'pages/analytics_exploration/components/classification_exploration/index'; @import 'pages/analytics_management/components/analytics_list/index'; @import 'pages/analytics_management/components/create_analytics_form/index'; @import 'pages/analytics_management/components/create_analytics_flyout/index'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts deleted file mode 100644 index fde854b7f41c3f..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/breadcrumbs.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -export function getDataFrameAnalyticsBreadcrumbs() { - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel', { - defaultMessage: 'Data Frame Analytics', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 0642c1fbe61864..cadc1f01c6dda3 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -35,6 +35,7 @@ interface ClassificationAnalysis { dependent_variable: string; training_percent?: number; num_top_classes?: string; + prediction_field_name?: string; }; } @@ -74,13 +75,33 @@ export interface RegressionEvaluateResponse { }; } +export interface PredictedClass { + predicted_class: string; + count: number; +} + +export interface ConfusionMatrix { + actual_class: string; + actual_class_doc_count: number; + predicted_classes: PredictedClass[]; + other_predicted_class_doc_count: number; +} + +export interface ClassificationEvaluateResponse { + classification: { + multiclass_confusion_matrix: { + confusion_matrix: ConfusionMatrix[]; + }; + }; +} + interface GenericAnalysis { [key: string]: Record; } interface LoadEvaluateResult { success: boolean; - eval: RegressionEvaluateResponse | null; + eval: RegressionEvaluateResponse | ClassificationEvaluateResponse | null; error: string | null; } @@ -109,6 +130,7 @@ export const getAnalysisType = (analysis: AnalysisConfig) => { export const getDependentVar = (analysis: AnalysisConfig) => { let depVar = ''; + if (isRegressionAnalysis(analysis)) { depVar = analysis.regression.dependent_variable; } @@ -124,17 +146,26 @@ export const getPredictionFieldName = (analysis: AnalysisConfig) => { let predictionFieldName; if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) { predictionFieldName = analysis.regression.prediction_field_name; + } else if ( + isClassificationAnalysis(analysis) && + analysis.classification.prediction_field_name !== undefined + ) { + predictionFieldName = analysis.classification.prediction_field_name; } return predictionFieldName; }; -export const getPredictedFieldName = (resultsField: string, analysis: AnalysisConfig) => { +export const getPredictedFieldName = ( + resultsField: string, + analysis: AnalysisConfig, + forSort?: boolean +) => { // default is 'ml' const predictionFieldName = getPredictionFieldName(analysis); const defaultPredictionField = `${getDependentVar(analysis)}_prediction`; const predictedField = `${resultsField}.${ predictionFieldName ? predictionFieldName : defaultPredictionField - }`; + }${isClassificationAnalysis(analysis) && !forSort ? '.keyword' : ''}`; return predictedField; }; @@ -153,13 +184,32 @@ export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysi return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; }; -export const isRegressionResultsSearchBoolQuery = ( - arg: any -): arg is RegressionResultsSearchBoolQuery => { +export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => { const keys = Object.keys(arg); return keys.length === 1 && keys[0] === 'bool'; }; +export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluateResponse => { + const keys = Object.keys(arg); + return ( + keys.length === 1 && + keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION && + arg?.regression?.mean_squared_error !== undefined && + arg?.regression?.r_squared !== undefined + ); +}; + +export const isClassificationEvaluateResponse = ( + arg: any +): arg is ClassificationEvaluateResponse => { + const keys = Object.keys(arg); + return ( + keys.length === 1 && + keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && + arg?.classification?.multiclass_confusion_matrix !== undefined + ); +}; + export interface DataFrameAnalyticsConfig { id: DataFrameAnalyticsId; // Description attribute is not supported yet @@ -254,17 +304,14 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) { return { meanSquaredError, rSquared }; } -interface RegressionResultsSearchBoolQuery { +interface ResultsSearchBoolQuery { bool: Dictionary; } -interface RegressionResultsSearchTermQuery { +interface ResultsSearchTermQuery { term: Dictionary; } -export type RegressionResultsSearchQuery = - | RegressionResultsSearchBoolQuery - | RegressionResultsSearchTermQuery - | SavedSearchQuery; +export type ResultsSearchQuery = ResultsSearchBoolQuery | ResultsSearchTermQuery | SavedSearchQuery; export function getEvalQueryBody({ resultsField, @@ -274,16 +321,16 @@ export function getEvalQueryBody({ }: { resultsField: string; isTraining: boolean; - searchQuery?: RegressionResultsSearchQuery; + searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; }) { - let query: RegressionResultsSearchQuery = { + let query: ResultsSearchQuery = { term: { [`${resultsField}.is_training`]: { value: isTraining } }, }; if (searchQuery !== undefined && ignoreDefaultQuery === true) { query = searchQuery; - } else if (searchQuery !== undefined && isRegressionResultsSearchBoolQuery(searchQuery)) { + } else if (searchQuery !== undefined && isResultsSearchBoolQuery(searchQuery)) { const searchQueryClone = cloneDeep(searchQuery); searchQueryClone.bool.must.push(query); query = searchQueryClone; @@ -291,6 +338,27 @@ export function getEvalQueryBody({ return query; } +interface EvaluateMetrics { + classification: { + multiclass_confusion_matrix: object; + }; + regression: { + r_squared: object; + mean_squared_error: object; + }; +} + +interface LoadEvalDataConfig { + isTraining: boolean; + index: string; + dependentVariable: string; + resultsField: string; + predictionFieldName?: string; + searchQuery?: ResultsSearchQuery; + ignoreDefaultQuery?: boolean; + jobType: ANALYSIS_CONFIG_TYPE; +} + export const loadEvalData = async ({ isTraining, index, @@ -299,34 +367,38 @@ export const loadEvalData = async ({ predictionFieldName, searchQuery, ignoreDefaultQuery, -}: { - isTraining: boolean; - index: string; - dependentVariable: string; - resultsField: string; - predictionFieldName?: string; - searchQuery?: RegressionResultsSearchQuery; - ignoreDefaultQuery?: boolean; -}) => { + jobType, +}: LoadEvalDataConfig) => { const results: LoadEvaluateResult = { success: false, eval: null, error: null }; const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ + let predictedField = `${resultsField}.${ predictionFieldName ? predictionFieldName : defaultPredictionField }`; + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { + predictedField = `${predictedField}.keyword`; + } + const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery }); + const metrics: EvaluateMetrics = { + classification: { + multiclass_confusion_matrix: {}, + }, + regression: { + r_squared: {}, + mean_squared_error: {}, + }, + }; + const config = { index, query, evaluation: { - regression: { + [jobType]: { actual_field: dependentVariable, predicted_field: predictedField, - metrics: { - r_squared: {}, - mean_squared_error: {}, - }, + metrics: metrics[jobType as keyof EvaluateMetrics], }, }, }; @@ -341,3 +413,57 @@ export const loadEvalData = async ({ return results; } }; + +interface TrackTotalHitsSearchResponse { + hits: { + total: { + value: number; + relation: string; + }; + hits: any[]; + }; +} + +interface LoadDocsCountConfig { + ignoreDefaultQuery?: boolean; + isTraining: boolean; + searchQuery: SavedSearchQuery; + resultsField: string; + destIndex: string; +} + +interface LoadDocsCountResponse { + docsCount: number | null; + success: boolean; +} + +export const loadDocsCount = async ({ + ignoreDefaultQuery = true, + isTraining, + searchQuery, + resultsField, + destIndex, +}: LoadDocsCountConfig): Promise => { + const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); + + try { + const body: SearchQuery = { + track_total_hits: true, + query, + }; + + const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ + index: destIndex, + size: 0, + body, + }); + + const docsCount = resp.hits.total && resp.hits.total.value; + return { docsCount, success: docsCount !== undefined }; + } catch (e) { + return { + docsCount: null, + success: false, + }; + } +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 5621d77f664697..216836db4ccbc2 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -77,7 +77,7 @@ export const sortRegressionResultsFields = ( ) => { const dependentVariable = getDependentVar(jobConfig.analysis); const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); if (a === `${resultsField}.is_training`) { return -1; } @@ -96,6 +96,14 @@ export const sortRegressionResultsFields = ( if (b === dependentVariable) { return 1; } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } + return a.localeCompare(b); }; @@ -107,7 +115,7 @@ export const sortRegressionResultsColumns = ( ) => (a: string, b: string) => { const dependentVariable = getDependentVar(jobConfig.analysis); const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); const typeofA = typeof obj[a]; const typeofB = typeof obj[b]; @@ -136,6 +144,14 @@ export const sortRegressionResultsColumns = ( return 1; } + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + + if (b === `${resultsField}.prediction_probability`) { + return 1; + } + if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -184,6 +200,43 @@ export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFi return flatDocFields.filter(f => f !== ML__ID_COPY); } +export const getDefaultClassificationFields = ( + docs: EsDoc[], + jobConfig: DataFrameAnalyticsConfig +): EsFieldName[] => { + if (docs.length === 0) { + return []; + } + const resultsField = jobConfig.dest.results_field; + const newDocFields = getFlattenedFields(docs[0]._source, resultsField); + return newDocFields + .filter(k => { + if (k === `${resultsField}.is_training`) { + return true; + } + // predicted value of dependent variable + if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { + return true; + } + // actual value of dependent variable + if (k === getDependentVar(jobConfig.analysis)) { + return true; + } + + if (k === `${resultsField}.prediction_probability`) { + return true; + } + + if (k.split('.')[0] === resultsField) { + return false; + } + + return docs.some(row => row._source[k] !== null); + }) + .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) + .slice(0, DEFAULT_REGRESSION_COLUMNS); +}; + export const getDefaultRegressionFields = ( docs: EsDoc[], jobConfig: DataFrameAnalyticsConfig diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts index 02a1c30259cced..f7794af8b5861a 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -20,6 +20,7 @@ export { RegressionEvaluateResponse, getValuesFromResponse, loadEvalData, + loadDocsCount, Eval, getPredictedFieldName, INDEX_STATUS, @@ -31,6 +32,7 @@ export { export { getDefaultSelectableFields, getDefaultRegressionFields, + getDefaultClassificationFields, getFlattenedFields, sortColumns, sortRegressionResultsColumns, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss new file mode 100644 index 00000000000000..1141dddf398b01 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_classification_exploration.scss @@ -0,0 +1,4 @@ +.euiFormRow.mlDataFrameAnalyticsClassification__actualLabel { + padding-top: $euiSize * 4; +} + diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss new file mode 100644 index 00000000000000..88edd92951d41c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/_index.scss @@ -0,0 +1 @@ +@import 'classification_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx new file mode 100644 index 00000000000000..f424ebee581204 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -0,0 +1,120 @@ +/* + * 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, { FC, Fragment, useState, useEffect } from 'react'; +import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ml } from '../../../../../services/ml_api_service'; +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { EvaluatePanel } from './evaluate_panel'; +import { ResultsTable } from './results_table'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; + +interface GetDataFrameAnalyticsResponse { + count: number; + data_frame_analytics: DataFrameAnalyticsConfig[]; +} + +export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( + + + {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { + defaultMessage: 'Destination index for classification job ID {jobId}', + values: { jobId }, + })} + + +); + +interface Props { + jobId: string; + jobStatus: DATA_FRAME_TASK_STATE; +} + +export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { + const [jobConfig, setJobConfig] = useState(undefined); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + const loadJobConfig = async () => { + setIsLoadingJobConfig(true); + try { + const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics( + jobId + ); + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIsLoadingJobConfig(false); + } else { + setJobConfigErrorMessage( + i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', + { + defaultMessage: 'No results found.', + } + ) + ); + } + } catch (e) { + if (e.message !== undefined) { + setJobConfigErrorMessage(e.message); + } else { + setJobConfigErrorMessage(JSON.stringify(e)); + } + setIsLoadingJobConfig(false); + } + }; + + useEffect(() => { + loadJobConfig(); + }, []); + + if (jobConfigErrorMessage !== undefined) { + return ( + + + + +

{jobConfigErrorMessage}

+
+
+ ); + } + + return ( + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && ( + + )} + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx new file mode 100644 index 00000000000000..5a08dd159affb3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/column_data.tsx @@ -0,0 +1,80 @@ +/* + * 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 { ConfusionMatrix, PredictedClass } from '../../../../common/analytics'; + +interface ColumnData { + actual_class: string; + actual_class_doc_count: number; + predicted_class?: string; + count?: number; + error_count?: number; +} + +export function getColumnData(confusionMatrixData: ConfusionMatrix[]) { + const colData: Partial = []; + + confusionMatrixData.forEach((classData: any) => { + const correctlyPredictedClass = classData.predicted_classes.find( + (pc: PredictedClass) => pc.predicted_class === classData.actual_class + ); + const incorrectlyPredictedClass = classData.predicted_classes.find( + (pc: PredictedClass) => pc.predicted_class !== classData.actual_class + ); + + let accuracy; + if (correctlyPredictedClass !== undefined) { + accuracy = correctlyPredictedClass.count / classData.actual_class_doc_count; + // round to 2 decimal places without converting to string; + accuracy = Math.round(accuracy * 100) / 100; + } + + let error; + if (incorrectlyPredictedClass !== undefined) { + error = incorrectlyPredictedClass.count / classData.actual_class_doc_count; + error = Math.round(error * 100) / 100; + } + + let col: any = { + actual_class: classData.actual_class, + actual_class_doc_count: classData.actual_class_doc_count, + }; + + if (correctlyPredictedClass !== undefined) { + col = { + ...col, + predicted_class: correctlyPredictedClass.predicted_class, + [correctlyPredictedClass.predicted_class]: accuracy, + count: correctlyPredictedClass.count, + accuracy, + }; + } + + if (incorrectlyPredictedClass !== undefined) { + col = { + ...col, + [incorrectlyPredictedClass.predicted_class]: error, + error_count: incorrectlyPredictedClass.count, + }; + } + + colData.push(col); + }); + + const columns: any = [ + { + id: 'actual_class', + display: , + }, + ]; + + colData.forEach((data: any) => { + columns.push({ id: data.predicted_class }); + }); + + return { columns, columnData: colData }; +} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx new file mode 100644 index 00000000000000..ddf52943c2feb2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -0,0 +1,343 @@ +/* + * 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, { FC, useState, useEffect, Fragment } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ErrorCallout } from '../error_callout'; +import { + getDependentVar, + getPredictionFieldName, + loadEvalData, + loadDocsCount, + DataFrameAnalyticsConfig, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; +import { + isResultsSearchBoolQuery, + isClassificationEvaluateResponse, + ConfusionMatrix, + ResultsSearchQuery, + ANALYSIS_CONFIG_TYPE, +} from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; +import { getColumnData } from './column_data'; + +const defaultPanelWidth = 500; + +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { + const [isLoading, setIsLoading] = useState(false); + const [confusionMatrixData, setConfusionMatrixData] = useState([]); + const [columns, setColumns] = useState([]); + const [columnsData, setColumnsData] = useState([]); + const [popoverContents, setPopoverContents] = useState([]); + const [docsCount, setDocsCount] = useState(null); + const [error, setError] = useState(null); + const [panelWidth, setPanelWidth] = useState(defaultPanelWidth); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState(() => + columns.map(({ id }: { id: string }) => id) + ); + + const index = jobConfig.dest.index; + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + // default is 'ml' + const resultsField = jobConfig.dest.results_field; + + const loadData = async ({ + isTrainingClause, + ignoreDefaultQuery = true, + }: { + isTrainingClause: { query: string; operator: string }; + ignoreDefaultQuery?: boolean; + }) => { + setIsLoading(true); + + const evalData = await loadEvalData({ + isTraining: false, + index, + dependentVariable, + resultsField, + predictionFieldName, + searchQuery, + ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION, + }); + + const docsCountResp = await loadDocsCount({ + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + + if ( + evalData.success === true && + evalData.eval && + isClassificationEvaluateResponse(evalData.eval) + ) { + const confusionMatrix = + evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix; + setError(null); + setConfusionMatrixData(confusionMatrix || []); + setIsLoading(false); + } else { + setIsLoading(false); + setConfusionMatrixData([]); + setError(evalData.error); + } + + if (docsCountResp.success === true) { + setDocsCount(docsCountResp.docsCount); + } else { + setDocsCount(null); + } + }; + + const resizeHandler = () => { + const tablePanelWidth: number = + document.getElementById('mlDataFrameAnalyticsTableResultsPanel')?.clientWidth || + defaultPanelWidth; + // Keep the evaluate panel width slightly smaller than the results table + // to ensure results table can resize correctly. Temporary workaround DataGrid issue with flex + const newWidth = tablePanelWidth - 8; + setPanelWidth(newWidth); + }; + + useEffect(() => { + window.addEventListener('resize', resizeHandler); + resizeHandler(); + return () => { + window.removeEventListener('resize', resizeHandler); + }; + }, []); + + useEffect(() => { + if (confusionMatrixData.length > 0) { + const { columns: derivedColumns, columnData } = getColumnData(confusionMatrixData); + // Initialize all columns as visible + setVisibleColumns(() => derivedColumns.map(({ id }: { id: string }) => id)); + setColumns(derivedColumns); + setColumnsData(columnData); + setPopoverContents({ + numeric: ({ + cellContentsElement, + children, + }: { + cellContentsElement: any; + children: any; + }) => { + const rowIndex = children?.props?.rowIndex; + const colId = children?.props?.columnId; + const gridItem = columnData[rowIndex]; + + if (gridItem !== undefined) { + const count = colId === gridItem.actual_class ? gridItem.count : gridItem.error_count; + return `${count} / ${gridItem.actual_class_doc_count} * 100 = ${cellContentsElement.textContent}`; + } + + return cellContentsElement.textContent; + }, + }); + } + }, [confusionMatrixData]); + + useEffect(() => { + const hasIsTrainingClause = + isResultsSearchBoolQuery(searchQuery) && + searchQuery.bool.must.filter( + (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined + ); + const isTrainingClause = + hasIsTrainingClause && + hasIsTrainingClause[0] && + hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + + loadData({ isTrainingClause }); + }, [JSON.stringify(searchQuery)]); + + const renderCellValue = ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const cellValue = columnsData[rowIndex][columnId]; + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + setCellProps({ + style: { + backgroundColor: `rgba(0, 179, 164, ${cellValue})`, + }, + }); + }, [rowIndex, columnId, setCellProps]); + return ( + {typeof cellValue === 'number' ? `${Math.round(cellValue * 100)}%` : cellValue} + ); + }; + + if (isLoading === true) { + return ; + } + + return ( + + + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle', + { + defaultMessage: 'Evaluation of classification job ID {jobId}', + values: { jobId: jobConfig.id }, + } + )} + + + + + {getTaskStateBadge(jobStatus)} + + + + {error !== null && ( + + + + )} + {error === null && ( + + + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText', + { + defaultMessage: 'Normalized confusion matrix', + } + )} + + + + + + + + {docsCount !== null && ( + + + + + + )} + {/* BEGIN TABLE ELEMENTS */} + + + + + + + + + {columns.length > 0 && columnsData.length > 0 && ( + + + + + + + + + + + + + + + + + + + + + + + )} + + + + + )} + {/* END TABLE ELEMENTS */} + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/index.ts new file mode 100644 index 00000000000000..4c75d8315b230b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/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 { ClassificationExploration } from './classification_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx new file mode 100644 index 00000000000000..1be158499a3f4c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx @@ -0,0 +1,481 @@ +/* + * 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, FC, useEffect, useState } from 'react'; +import moment from 'moment-timezone'; + +import { i18n } from '@kbn/i18n'; +import { + EuiBadge, + EuiButtonIcon, + EuiCallOut, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiPopover, + EuiPopoverTitle, + EuiProgress, + EuiSpacer, + EuiText, + EuiToolTip, + Query, +} from '@elastic/eui'; + +import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common'; + +import { + ColumnType, + mlInMemoryTableBasicFactory, + OnTableChangeArg, + SortingPropType, + SORT_DIRECTION, +} from '../../../../../components/ml_in_memory_table'; + +import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + sortRegressionResultsColumns, + sortRegressionResultsFields, + toggleSelectedField, + DataFrameAnalyticsConfig, + EsFieldName, + EsDoc, + MAX_COLUMNS, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, +} from '../../../../common'; +import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { useExploreData, TableItem } from './use_explore_data'; +import { ExplorationTitle } from './classification_exploration'; + +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); +interface Props { + jobConfig: DataFrameAnalyticsConfig; + jobStatus: DATA_FRAME_TASK_STATE; + setEvaluateSearchQuery: React.Dispatch>; +} + +export const ResultsTable: FC = React.memo( + ({ jobConfig, jobStatus, setEvaluateSearchQuery }) => { + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(25); + const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); + const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchError, setSearchError] = useState(undefined); + const [searchString, setSearchString] = useState(undefined); + + function toggleColumnsPopover() { + setColumnsPopoverVisible(!isColumnsPopoverVisible); + } + + function closeColumnsPopover() { + setColumnsPopoverVisible(false); + } + + function toggleColumn(column: EsFieldName) { + if (tableItems.length > 0 && jobConfig !== undefined) { + // spread to a new array otherwise the component wouldn't re-render + setSelectedFields([...toggleSelectedField(selectedFields, column)]); + } + } + + const { + errorMessage, + loadExploreData, + sortField, + sortDirection, + status, + tableItems, + } = useExploreData(jobConfig, selectedFields, setSelectedFields); + + let docFields: EsFieldName[] = []; + let docFieldsCount = 0; + if (tableItems.length > 0) { + docFields = Object.keys(tableItems[0]); + docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)); + docFieldsCount = docFields.length; + } + + const columns: Array> = []; + + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { + columns.push( + ...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => { + const column: ColumnType = { + field: k, + name: k, + sortable: true, + truncateText: true, + }; + + const render = (d: any, fullItem: EsDoc) => { + if (Array.isArray(d) && d.every(item => typeof item === 'string')) { + // If the cells data is an array of strings, return as a comma separated list. + // The list will get limited to 5 items with `…` at the end if there's more in the original array. + return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`; + } else if (Array.isArray(d)) { + // If the cells data is an array of e.g. objects, display a 'array' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent', + { + defaultMessage: 'array', + } + )} + + + ); + } else if (typeof d === 'object' && d !== null) { + // If the cells data is an object, display a 'object' badge with a + // tooltip that explains that this type of field is not supported in this table. + return ( + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.indexObjectBadgeContent', + { + defaultMessage: 'object', + } + )} + + + ); + } + + return d; + }; + + let columnType; + + if (tableItems.length > 0) { + columnType = typeof tableItems[0][k]; + } + + if (typeof columnType !== 'undefined') { + switch (columnType) { + case 'boolean': + column.dataType = 'boolean'; + break; + case 'Date': + column.align = 'right'; + column.render = (d: any) => + formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000); + break; + case 'number': + column.dataType = 'number'; + column.render = render; + break; + default: + column.render = render; + break; + } + } else { + column.render = render; + } + + return column; + }) + ); + } + + useEffect(() => { + if (jobConfig !== undefined) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [JSON.stringify(searchQuery)]); + + useEffect(() => { + // by default set the sorting to descending on the prediction field (`_prediction`). + // if that's not available sort ascending on the first column. + // also check if the current sorting field is still available. + if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) { + const predictedFieldName = getPredictedFieldName( + jobConfig.dest.results_field, + jobConfig.analysis + ); + const predictedFieldSelected = selectedFields.includes(predictedFieldName); + + const field = predictedFieldSelected ? predictedFieldName : selectedFields[0]; + const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC; + loadExploreData({ field, direction, searchQuery }); + } + }, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]); + + let sorting: SortingPropType = false; + let onTableChange; + + if (columns.length > 0 && sortField !== '' && sortField !== undefined) { + sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + onTableChange = ({ + page = { index: 0, size: 10 }, + sort = { field: sortField, direction: sortDirection }, + }: OnTableChangeArg) => { + const { index, size } = page; + setPageIndex(index); + setPageSize(size); + + if (sort.field !== sortField || sort.direction !== sortDirection) { + loadExploreData({ ...sort, searchQuery }); + } + }; + } + + const pagination = { + initialPageIndex: pageIndex, + initialPageSize: pageSize, + totalItemCount: tableItems.length, + pageSizeOptions: PAGE_SIZE_OPTIONS, + hidePerPageOptions: false, + }; + + const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => { + if (error) { + setSearchError(error.message); + } else { + try { + const esQueryDsl = Query.toESQuery(query); + setSearchQuery(esQueryDsl); + setSearchString(query.text); + setSearchError(undefined); + // set query for use in evaluate panel + setEvaluateSearchQuery(esQueryDsl); + } catch (e) { + setSearchError(e.toString()); + } + } + }; + + const search = { + onChange: onQueryChange, + defaultQuery: searchString, + box: { + incremental: false, + placeholder: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder', + { + defaultMessage: 'E.g. avg>0.5', + } + ), + }, + filters: [ + { + type: 'field_value_toggle_group', + field: `${jobConfig.dest.results_field}.is_training`, + items: [ + { + value: false, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel', + { + defaultMessage: 'Testing', + } + ), + }, + { + value: true, + name: i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel', + { + defaultMessage: 'Training', + } + ), + }, + ], + }, + ], + }; + + if (jobConfig === undefined) { + return null; + } + // if it's a searchBar syntax error leave the table visible so they can try again + if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { + return ( + + + + + + + {getTaskStateBadge(jobStatus)} + + + +

{errorMessage}

+
+
+ ); + } + + const tableError = + status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') + ? errorMessage + : searchError; + + return ( + + + + + + + + + {getTaskStateBadge(jobStatus)} + + + + + + + {docFieldsCount > MAX_COLUMNS && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', + { + defaultMessage: + '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', + values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + } + )} + + )} + + + + + } + isOpen={isColumnsPopoverVisible} + closePopover={closeColumnsPopover} + ownFocus + > + + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle', + { + defaultMessage: 'Select fields', + } + )} + +
+ {docFields.map(d => ( + toggleColumn(d)} + disabled={selectedFields.includes(d) && selectedFields.length === 1} + /> + ))} +
+
+
+
+
+
+
+ {status === INDEX_STATUS.LOADING && } + {status !== INDEX_STATUS.LOADING && ( + + )} + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + + {tableItems.length === SEARCH_SIZE && ( + + + + )} + + + + )} +
+ ); + } +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts new file mode 100644 index 00000000000000..ba12fcab98a36d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts @@ -0,0 +1,156 @@ +/* + * 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. + */ +/* + * 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, useState } from 'react'; + +import { SearchResponse } from 'elasticsearch'; + +import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; + +import { ml } from '../../../../../services/ml_api_service'; +import { getNestedProperty } from '../../../../../util/object_utils'; +import { SavedSearchQuery } from '../../../../../contexts/kibana'; + +import { + getDefaultClassificationFields, + getFlattenedFields, + DataFrameAnalyticsConfig, + EsFieldName, + getPredictedFieldName, + INDEX_STATUS, + SEARCH_SIZE, + defaultSearchQuery, + SearchQuery, +} from '../../../../common'; + +export type TableItem = Record; + +interface LoadExploreDataArg { + field: string; + direction: SortDirection; + searchQuery: SavedSearchQuery; +} +export interface UseExploreDataReturnType { + errorMessage: string; + loadExploreData: (arg: LoadExploreDataArg) => void; + sortField: EsFieldName; + sortDirection: SortDirection; + status: INDEX_STATUS; + tableItems: TableItem[]; +} + +export const useExploreData = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + selectedFields: EsFieldName[], + setSelectedFields: React.Dispatch> +): UseExploreDataReturnType => { + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [tableItems, setTableItems] = useState([]); + const [sortField, setSortField] = useState(''); + const [sortDirection, setSortDirection] = useState(SORT_DIRECTION.ASC); + + const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => { + if (jobConfig !== undefined) { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resultsField = jobConfig.dest.results_field; + const body: SearchQuery = { + query: searchQuery, + }; + + if (field !== undefined) { + body.sort = [ + { + [field]: { + order: direction, + }, + }, + ]; + } + + const resp: SearchResponse = await ml.esSearch({ + index: jobConfig.dest.index, + size: SEARCH_SIZE, + body, + }); + + setSortField(field); + setSortDirection(direction); + + const docs = resp.hits.hits; + + if (docs.length === 0) { + setTableItems([]); + setStatus(INDEX_STATUS.LOADED); + return; + } + + if (selectedFields.length === 0) { + const newSelectedFields = getDefaultClassificationFields(docs, jobConfig); + setSelectedFields(newSelectedFields); + } + + // Create a version of the doc's source with flattened field names. + // This avoids confusion later on if a field name has dots in its name + // or is a nested fields when displaying it via EuiInMemoryTable. + const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); + const transformedTableItems = docs.map(doc => { + const item: TableItem = {}; + flattenedFields.forEach(ff => { + item[ff] = getNestedProperty(doc._source, ff); + if (item[ff] === undefined) { + // If the attribute is undefined, it means it was not a nested property + // but had dots in its actual name. This selects the property by its + // full name and assigns it to `item[ff]`. + item[ff] = doc._source[`"${ff}"`]; + } + if (item[ff] === undefined) { + const parts = ff.split('.'); + if (parts[0] === resultsField && parts.length >= 2) { + parts.shift(); + if (doc._source[resultsField] !== undefined) { + item[ff] = doc._source[resultsField][parts.join('.')]; + } + } + } + }); + return item; + }); + + setTableItems(transformedTableItems); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + if (e.message !== undefined) { + setErrorMessage(e.message); + } else { + setErrorMessage(JSON.stringify(e)); + } + setTableItems([]); + setStatus(INDEX_STATUS.ERROR); + } + } + }; + + useEffect(() => { + if (jobConfig !== undefined) { + loadExploreData({ + field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis), + direction: SORT_DIRECTION.DESC, + searchQuery: defaultSearchQuery, + }); + } + }, [jobConfig && jobConfig.id]); + + return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx similarity index 100% rename from x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/error_callout.tsx rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/index.ts new file mode 100644 index 00000000000000..4f86d0d061c976 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/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 { ErrorCallout } from './error_callout'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx index c4bba08353d843..31e6d409b1c4fd 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration/exploration.tsx @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useEffect, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; -import d3 from 'd3'; - import { EuiBadge, EuiButtonIcon, @@ -18,7 +16,6 @@ import { EuiCheckbox, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiPanel, EuiPopover, EuiPopoverTitle, @@ -30,9 +27,12 @@ import { Query, } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; - +import { + useColorRange, + ColorRangeLegend, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; import { ColumnType, mlInMemoryTableBasicFactory, @@ -41,8 +41,6 @@ import { SORT_DIRECTION, } from '../../../../../components/ml_in_memory_table'; -import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context'; - import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils'; import { ml } from '../../../../../services/ml_api_service'; @@ -67,16 +65,6 @@ import { import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { SavedSearchQuery } from '../../../../../contexts/kibana'; -const customColorScaleFactory = (n: number) => (t: number) => { - if (t < 1 / n) { - return 0; - } - if (t < 3 / n) { - return (n / 4) * (t - 1 / n); - } - return 0.5 + (t - 3 / n); -}; - const FEATURE_INFLUENCE = 'feature_influence'; interface GetDataFrameAnalyticsResponse { @@ -102,6 +90,16 @@ interface Props { jobStatus: DATA_FRAME_TASK_STATE; } +const getFeatureCount = (jobConfig?: DataFrameAnalyticsConfig, tableItems: TableItem[] = []) => { + if (jobConfig === undefined || tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`) + ).length; +}; + export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); @@ -126,12 +124,6 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { })(); }, []); - const euiTheme = useUiChromeContext() - .getUiSettingsClient() - .get('theme:darkMode') - ? euiThemeDark - : euiThemeLight; - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false); @@ -169,23 +161,13 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { const columns: Array> = []; - if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { - // table cell color coding takes into account: - // - whether the theme is dark/light - // - the number of analysis features - // based on that - const cellBgColorScale = d3.scale - .linear() - .domain([0, 1]) - // typings for .range() incorrectly don't allow passing in a color extent. - // @ts-ignore - .range([d3.rgb(euiTheme.euiColorEmptyShade), d3.rgb(euiTheme.euiColorVis1)]); - const featureCount = Object.keys(tableItems[0]).filter(key => - key.includes(`${jobConfig.dest.results_field}.${FEATURE_INFLUENCE}.`) - ).length; - const customScale = customColorScaleFactory(featureCount); - const cellBgColor = (n: number) => cellBgColorScale(customScale(n)); + const cellBgColor = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + getFeatureCount(jobConfig, tableItems) + ); + if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) { columns.push( ...selectedFields.sort(sortColumns(tableItems[0], jobConfig.dest.results_field)).map(k => { const column: ColumnType = { @@ -504,21 +486,34 @@ export const Exploration: FC = React.memo(({ jobId, jobStatus }) => { )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && sortField !== '' && ( - - {tableItems.length === SEARCH_SIZE && ( - + + + + {tableItems.length === SEARCH_SIZE && ( + + {i18n.translate( + 'xpack.ml.dataframe.analytics.exploration.documentsShownHelpText', + { + defaultMessage: 'Showing first {searchSize} documents', + values: { searchSize: SEARCH_SIZE }, + } + )} + )} - > - - - )} - + + + + + = React.memo(({ jobId, jobStatus }) => { search={search} error={tableError} /> - + )} ); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/index.ts new file mode 100644 index 00000000000000..40b9e000d6b07c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/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 { LoadingPanel } from './loading_panel'; diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx similarity index 51% rename from x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx index 383170a13d4cfa..f71fbc944f0ed9 100644 --- a/x-pack/legacy/plugins/graph/server/lib/pre/get_call_cluster_pre.js +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/loading_panel/loading_panel.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export const getCallClusterPre = { - assign: 'callCluster', - method(request) { - const cluster = request.server.plugins.elasticsearch.getCluster('data'); - return (...args) => cluster.callWithRequest(request, ...args); - } -}; +import React, { FC } from 'react'; +import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; + +export const LoadingPanel: FC = () => ( + + + +); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index d877ed40e587d5..a8a015c6ef3455 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -8,40 +8,30 @@ import React, { FC, Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; -import { ErrorCallout } from './error_callout'; +import { ErrorCallout } from '../error_callout'; import { getValuesFromResponse, getDependentVar, getPredictionFieldName, loadEvalData, + loadDocsCount, Eval, DataFrameAnalyticsConfig, } from '../../../../common'; -import { ml } from '../../../../../services/ml_api_service'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { EvaluateStat } from './evaluate_stat'; import { - getEvalQueryBody, - isRegressionResultsSearchBoolQuery, - RegressionResultsSearchQuery, - SearchQuery, + isResultsSearchBoolQuery, + isRegressionEvaluateResponse, + ResultsSearchQuery, + ANALYSIS_CONFIG_TYPE, } from '../../../../common/analytics'; interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; - searchQuery: RegressionResultsSearchQuery; -} - -interface TrackTotalHitsSearchResponse { - hits: { - total: { - value: number; - relation: string; - }; - hits: any[]; - }; + searchQuery: ResultsSearchQuery; } const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null }; @@ -60,40 +50,6 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) // default is 'ml' const resultsField = jobConfig.dest.results_field; - const loadDocsCount = async ({ - ignoreDefaultQuery = true, - isTraining, - }: { - ignoreDefaultQuery?: boolean; - isTraining: boolean; - }): Promise<{ - docsCount: number | null; - success: boolean; - }> => { - const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery }); - - try { - const body: SearchQuery = { - track_total_hits: true, - query, - }; - - const resp: TrackTotalHitsSearchResponse = await ml.esSearch({ - index: jobConfig.dest.index, - size: 0, - body, - }); - - const docsCount = resp.hits.total && resp.hits.total.value; - return { docsCount, success: true }; - } catch (e) { - return { - docsCount: null, - success: false, - }; - } - }; - const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => { setIsLoadingGeneralization(true); @@ -105,9 +61,14 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) predictionFieldName, searchQuery, ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (genErrorEval.success === true && genErrorEval.eval) { + if ( + genErrorEval.success === true && + genErrorEval.eval && + isRegressionEvaluateResponse(genErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ meanSquaredError, @@ -136,9 +97,14 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) predictionFieldName, searchQuery, ignoreDefaultQuery, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (trainingErrorEval.success === true && trainingErrorEval.eval) { + if ( + trainingErrorEval.success === true && + trainingErrorEval.eval && + isRegressionEvaluateResponse(trainingErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ meanSquaredError, @@ -165,7 +131,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) if (isTrainingClause !== undefined && isTrainingClause.query === 'false') { loadGeneralizationData(); - const docsCountResp = await loadDocsCount({ isTraining: false }); + const docsCountResp = await loadDocsCount({ + isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (docsCountResp.success === true) { setGeneralizationDocsCount(docsCountResp.docsCount); } else { @@ -182,7 +154,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) // searchBar query is filtering for training data loadTrainingData(); - const docsCountResp = await loadDocsCount({ isTraining: true }); + const docsCountResp = await loadDocsCount({ + isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, + }); + if (docsCountResp.success === true) { setTrainingDocsCount(docsCountResp.docsCount); } else { @@ -201,6 +179,9 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const genDocsCountResp = await loadDocsCount({ ignoreDefaultQuery: false, isTraining: false, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, }); if (genDocsCountResp.success === true) { setGeneralizationDocsCount(genDocsCountResp.docsCount); @@ -212,6 +193,9 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const trainDocsCountResp = await loadDocsCount({ ignoreDefaultQuery: false, isTraining: true, + searchQuery, + resultsField, + destIndex: jobConfig.dest.index, }); if (trainDocsCountResp.success === true) { setTrainingDocsCount(trainDocsCountResp.docsCount); @@ -223,7 +207,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) useEffect(() => { const hasIsTrainingClause = - isRegressionResultsSearchBoolQuery(searchQuery) && + isResultsSearchBoolQuery(searchQuery) && searchQuery.bool.must.filter( (clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined ); @@ -241,10 +225,13 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { - defaultMessage: 'Regression job ID {jobId}', - values: { jobId: jobConfig.id }, - })} + {i18n.translate( + 'xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle', + { + defaultMessage: 'Evaluation of regression job ID {jobId}', + values: { jobId: jobConfig.id }, + } + )} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 2f7ff4feed2a8e..12a41e1e7d851e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -5,31 +5,26 @@ */ import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ml } from '../../../../../services/ml_api_service'; import { DataFrameAnalyticsConfig } from '../../../../common'; import { EvaluatePanel } from './evaluate_panel'; import { ResultsTable } from './results_table'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; +import { LoadingPanel } from '../loading_panel'; interface GetDataFrameAnalyticsResponse { count: number; data_frame_analytics: DataFrameAnalyticsConfig[]; } -const LoadingPanel: FC = () => ( - - - -); - export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', { - defaultMessage: 'Regression job ID {jobId}', + {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { + defaultMessage: 'Destination index for regression job ID {jobId}', values: { jobId }, })} @@ -45,7 +40,7 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { const [jobConfig, setJobConfig] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const loadJobConfig = async () => { setIsLoadingJobConfig(true); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 37c2e40c89c3c2..1828297365f7a6 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -60,6 +60,8 @@ import { ExplorationTitle } from './regression_exploration'; const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; +const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); + interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus: DATA_FRAME_TASK_STATE; @@ -363,8 +365,6 @@ export const ResultsTable: FC = React.memo( ? errorMessage : searchError; - const MlInMemoryTableBasic = mlInMemoryTableBasicFactory(); - return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts index 3a83ad238d0e19..8e9cf45c14ec77 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts @@ -3,11 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -/* - * 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, useState } from 'react'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx deleted file mode 100644 index 1d4ac85ae2e872..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/directive.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsExploration', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const globalState = $injector.get('globalState'); - globalState.fetch(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts similarity index 88% rename from x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts index 38390172913265..7e2d651439ae30 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.js +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - -import './directive'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index b3d13db0a35505..b00a38e2b5f658 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -24,6 +24,7 @@ import { NavigationMenu } from '../../../components/navigation_menu'; import { Exploration } from './components/exploration'; import { RegressionExploration } from './components/regression_exploration'; +import { ClassificationExploration } from './components/classification_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common'; @@ -72,6 +73,9 @@ export const Page: FC<{ {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( )} + {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + )}
diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts deleted file mode 100644 index b705c604c190cc..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/route.ts +++ /dev/null @@ -1,30 +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 uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; -import { - loadCurrentIndexPattern, - loadCurrentSavedSearch, - loadIndexPatterns, -} from '../../../util/index_utils'; -import { getDataFrameAnalyticsBreadcrumbs } from '../../breadcrumbs'; - -const template = ``; - -uiRoutes.when('/data_frame_analytics/exploration?', { - template, - k7Breadcrumbs: getDataFrameAnalyticsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - indexPatterns: loadIndexPatterns, - savedSearch: loadCurrentSavedSearch, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index e189c961ccbc95..fc3c00cbcf3e3e 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -17,6 +17,7 @@ import { getAnalysisType, isRegressionAnalysis, isOutlierAnalysis, + isClassificationAnalysis, } from '../../../../common/analytics'; import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; @@ -31,7 +32,9 @@ export const AnalyticsViewAction = { const analysisType = getAnalysisType(item.config.analysis); const jobStatus = item.stats.state; const isDisabled = - !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis); + !isRegressionAnalysis(item.config.analysis) && + !isOutlierAnalysis(item.config.analysis) && + !isClassificationAnalysis(item.config.analysis); const url = getResultsUrl(item.id, analysisType, jobStatus); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index 91b73307ef56c3..8772be698bf58a 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -25,7 +25,11 @@ import { Eval, } from '../../../../common'; import { isCompletedAnalyticsJob } from './common'; -import { isRegressionAnalysis } from '../../../../common/analytics'; +import { + isRegressionAnalysis, + ANALYSIS_CONFIG_TYPE, + isRegressionEvaluateResponse, +} from '../../../../common/analytics'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; function getItemDescription(value: any) { @@ -81,9 +85,14 @@ export const ExpandedRow: FC = ({ item }) => { dependentVariable, resultsField, predictionFieldName, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (genErrorEval.success === true && genErrorEval.eval) { + if ( + genErrorEval.success === true && + genErrorEval.eval && + isRegressionEvaluateResponse(genErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval); setGeneralizationEval({ meanSquaredError, @@ -106,9 +115,14 @@ export const ExpandedRow: FC = ({ item }) => { dependentVariable, resultsField, predictionFieldName, + jobType: ANALYSIS_CONFIG_TYPE.REGRESSION, }); - if (trainingErrorEval.success === true && trainingErrorEval.eval) { + if ( + trainingErrorEval.success === true && + trainingErrorEval.eval && + isRegressionEvaluateResponse(trainingErrorEval.eval) + ) { const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval); setTrainingEval({ meanSquaredError, diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx deleted file mode 100644 index 5d97ed6dfcd3d4..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/directive.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; -import { createSearchItems } from '../../../jobs/new_job/utils/new_job_utils'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../../contexts/kibana'; - -import { Page } from './page'; - -module.directive('mlDataFrameAnalyticsManagement', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 7db9420396a9a8..754f7a1136a97c 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -20,7 +20,7 @@ type SourceIndex = DataFrameAnalyticsConfig['source']['index']; const getMockState = ({ index, - modelMemoryLimit, + modelMemoryLimit = '100mb', }: { index: SourceIndex; modelMemoryLimit?: string; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index eda80ca64c86fc..06c8a6c6a88465 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -56,7 +56,7 @@ const getSourceIndexString = (state: State) => { }; export const validateAdvancedEditor = (state: State): State => { - const { jobIdEmpty, jobIdValid, jobIdExists, createIndexPattern } = state.form; + const { jobIdEmpty, jobIdValid, jobIdExists, jobType, createIndexPattern } = state.form; const { jobConfig } = state; state.advancedEditorMessages = []; @@ -85,14 +85,25 @@ export const validateAdvancedEditor = (state: State): State => { const destinationIndexPatternTitleExists = state.indexPatternsMap[destinationIndexName] !== undefined; const mml = jobConfig.model_memory_limit; - const modelMemoryLimitEmpty = mml === ''; + const modelMemoryLimitEmpty = mml === '' || mml === undefined; if (!modelMemoryLimitEmpty && mml !== undefined) { const { valid } = validateModelMemoryLimitUnits(mml); state.form.modelMemoryLimitUnitValid = valid; } let dependentVariableEmpty = false; - if (isRegressionAnalysis(jobConfig.analysis) || isClassificationAnalysis(jobConfig.analysis)) { + + if ( + jobConfig.analysis === undefined && + (jobType === JOB_TYPES.CLASSIFICATION || jobType === JOB_TYPES.REGRESSION) + ) { + dependentVariableEmpty = true; + } + + if ( + jobConfig.analysis !== undefined && + (isRegressionAnalysis(jobConfig.analysis) || isClassificationAnalysis(jobConfig.analysis)) + ) { const dependentVariableName = getDependentVar(jobConfig.analysis) || ''; dependentVariableEmpty = dependentVariableName === ''; } diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts similarity index 87% rename from x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js rename to x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts index bcc62f4c5b10e2..7e2d651439ae30 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.js +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './list'; -import './edit'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts deleted file mode 100644 index 89e02eb4206392..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/route.ts +++ /dev/null @@ -1,29 +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 uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; -import { loadMlServerInfo } from '../../../services/ml_server_info'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; -import { getDataFrameAnalyticsBreadcrumbs } from '../../breadcrumbs'; - -const template = ``; - -uiRoutes.when('/data_frame_analytics/?', { - template, - k7Breadcrumbs: getDataFrameAnalyticsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts deleted file mode 100644 index a4d1fd37bc3383..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/breadcrumbs.ts +++ /dev/null @@ -1,13 +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 { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index e6f6d4581c7060..1727b1652c55d2 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -32,13 +32,13 @@ function startTrialDescription() { ), @@ -57,7 +57,7 @@ export const DatavisualizerSelector: FC = () => { return ( - + @@ -145,7 +145,7 @@ export const DatavisualizerSelector: FC = () => { footer={ - -`; - -uiRoutes.when('/datavisualizer', { - template, - k7Breadcrumbs: getDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - }, -}); - -import { DatavisualizerSelector } from './datavisualizer_selector'; - -module.directive('datavisualizerSelector', function() { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts deleted file mode 100644 index e8dd89f5db2647..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/breadcrumbs.ts +++ /dev/null @@ -1,23 +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 { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB } from '../../../breadcrumbs'; - -export function getFileDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { - defaultMessage: 'File', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index c89f618aa835bc..b50eef363847ec 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -359,7 +359,7 @@ export class ImportView extends Component { } async loadIndexPatternNames() { - await loadIndexPatterns(); + await loadIndexPatterns(this.props.indexPatterns); const indexPatternNames = getIndexPatternNames(); this.setState({ indexPatternNames }); } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js index 30e91783fae2cf..20b997582c3f9d 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.js @@ -121,7 +121,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} + href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`} /> } @@ -137,7 +137,7 @@ export class ResultsLinks extends Component { /> } description="" - href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} + href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`} /> } diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 99e61d5937c1d8..149e3d1818e642 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -6,26 +6,22 @@ import React, { FC, Fragment } from 'react'; import { timefilter } from 'ui/timefilter'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix } from '../../contexts/kibana'; import { NavigationMenu } from '../../components/navigation_menu'; +import { getIndexPatternsContract } from '../../util/index_utils'; // @ts-ignore import { FileDataVisualizerView } from './components/file_datavisualizer_view/index'; export interface FileDataVisualizerPageProps { - indexPatterns: IndexPatternsContract; kibanaConfig: KibanaConfigTypeFix; } -export const FileDataVisualizerPage: FC = ({ - indexPatterns, - kibanaConfig, -}) => { +export const FileDataVisualizerPage: FC = ({ kibanaConfig }) => { timefilter.disableTimeRangeSelector(); timefilter.disableAutoRefreshSelector(); - + const indexPatterns = getIndexPatternsContract(); return ( diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx deleted file mode 100644 index 7ca2db041da295..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer_directive.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; - -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { KibanaConfigTypeFix } from '../../contexts/kibana'; -import { getFileDataVisualizerBreadcrumbs } from './breadcrumbs'; -import { InjectorService } from '../../../../common/types/angular'; -import { checkBasicLicense } from '../../license/check_license'; -import { checkFindFileStructurePrivilege } from '../../privilege/check_privilege'; -import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; -import { loadMlServerInfo } from '../../services/ml_server_info'; -import { loadIndexPatterns } from '../../util/index_utils'; -import { FileDataVisualizerPage, FileDataVisualizerPageProps } from './file_datavisualizer'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; - -const template = ` -
- -`; - -uiRoutes.when('/filedatavisualizer/?', { - template, - k7Breadcrumbs: getFileDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - indexPatterns: loadIndexPatterns, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, -}); - -module.directive('fileDatavisualizerPage', function($injector: InjectorService) { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - - const props: FileDataVisualizerPageProps = { - indexPatterns, - kibanaConfig, - }; - ReactDOM.render( - {React.createElement(FileDataVisualizerPage, props)}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts index 15796ea9ff0bda..683d5e940aa7cd 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/file_based/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './file_datavisualizer_directive'; +export { FileDataVisualizerPage } from './file_datavisualizer'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts index dcda3ec9879aab..770b48973b1543 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; -import './file_based'; -import './index_based'; +export { DatavisualizerSelector } from './datavisualizer_selector'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.ts deleted file mode 100644 index aba45e04c638fd..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/breadcrumbs.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 { i18n } from '@kbn/i18n'; -import { - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - // @ts-ignore -} from '../../../breadcrumbs'; - -export function getDataVisualizerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { - defaultMessage: 'Index', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index fca2508cb5d14a..0b68f7e096d857 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -13,7 +13,6 @@ import { IndexPattern } from 'ui/index_patterns'; import { EuiPanel, EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; -import { useUiChromeContext } from '../../../../contexts/ui/use_ui_chrome_context'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -31,12 +30,10 @@ export const ActionsPanel: FC = ({ indexPattern }) => { }, }; - const basePath = useUiChromeContext().getBasePath(); - function openAdvancedJobWizard() { // TODO - pass the search string to the advanced job page as well as the index pattern // (add in with new advanced job wizard?) - window.open(`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); + window.open(`#/jobs/new_job/advanced?index=${indexPattern}`, '_self'); } // Note we use display:none for the DataRecognizer section as it needs to be @@ -87,7 +84,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { 'Use the full range of options to create a job for more advanced use cases', })} onClick={openAdvancedJobWizard} - href={`${basePath}/app/ml#/jobs/new_job/advanced?index=${indexPattern}`} + href={`#/jobs/new_job/advanced?index=${indexPattern}`} /> ); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx deleted file mode 100644 index 5de7cb6b71acb1..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/directive.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../src/plugins/data/public'; -import { InjectorService } from '../../../../common/types/angular'; - -import { KibanaConfigTypeFix, KibanaContext } from '../../contexts/kibana/kibana_context'; -import { createSearchItems } from '../../jobs/new_job/utils/new_job_utils'; - -import { Page } from './page'; - -module.directive('mlDataVisualizer', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts index 8ef2e327a89849..7e2d651439ae30 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; -import './route'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 99e128e9541030..898c852fe50a53 100644 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -31,7 +31,7 @@ import { FullTimeRangeSelector } from '../../components/full_time_range_selector import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; import { useKibanaContext, SavedSearchQuery } from '../../contexts/kibana'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; -import { timeBasedIndexCheck } from '../../util/index_utils'; +import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; import { TimeBuckets } from '../../util/time_buckets'; import { FieldRequestConfig, FieldVisConfig } from './common'; import { ActionsPanel } from './components/actions_panel'; @@ -173,9 +173,8 @@ export const Page: FC = () => { useEffect(() => { // Check for a saved search being passed in. - const searchSource = currentSavedSearch.searchSource; - const query = searchSource.getField('query'); - if (query !== undefined) { + if (currentSavedSearch !== null) { + const { query } = getQueryFromSavedSearch(currentSavedSearch); const queryLanguage = query.language as SEARCH_QUERY_LANGUAGE; const qryString = query.query; let qry; diff --git a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts b/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts deleted file mode 100644 index ab4df73e720ea3..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/datavisualizer/index_based/route.ts +++ /dev/null @@ -1,28 +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. - */ - -// @ts-ignore -import uiRoutes from 'ui/routes'; -import { checkBasicLicense } from '../../license/check_license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../util/index_utils'; - -import { checkMlNodesAvailable } from '../../ml_nodes_check'; -import { getDataVisualizerBreadcrumbs } from './breadcrumbs'; - -const template = ``; - -uiRoutes.when('/jobs/new_job/datavisualizer', { - template, - k7Breadcrumbs: getDataVisualizerBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts deleted file mode 100644 index c0dcd9e249b3bb..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/breadcrumbs.ts +++ /dev/null @@ -1,23 +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 { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; - -export function getAnomalyExplorerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { - defaultMessage: 'Anomaly Explorer', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js index 50a57f634fd1ba..d8a42064de4f2d 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/legacy/plugins/ml/public/application/explorer/explorer.js @@ -93,7 +93,7 @@ function mapSwimlaneOptionsToEuiOptions(options) { } const ExplorerPage = ({ children, jobSelectorProps, resizeRef }) => ( -
+
{children} @@ -117,7 +117,6 @@ export const Explorer = injectI18n(injectObservablesAsProps( }; _unsubscribeAll = new Subject(); - // make sure dragSelect is only available if the mouse pointer is actually over a swimlane disableDragSelectOnMouseLeave = true; diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx deleted file mode 100644 index b5d65fbf937e4d..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_directive.tsx +++ /dev/null @@ -1,110 +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. - */ - -/* - * AngularJS directive wrapper for rendering Anomaly Explorer's React component. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { Subscription } from 'rxjs'; - -import { IRootElementService, IRootScopeService, IScope } from 'angular'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { I18nContext } from 'ui/i18n'; -import { State } from 'ui/state_management/state'; -import { AppState as IAppState, AppStateClass } from 'ui/state_management/app_state'; - -import { jobSelectServiceFactory } from '../components/job_selector/job_select_service_utils'; - -import { interval$ } from '../components/controls/select_interval'; -import { severity$ } from '../components/controls/select_severity'; -import { showCharts$ } from '../components/controls/checkbox_showcharts'; -import { subscribeAppStateToObservable } from '../util/app_state_utils'; - -import { Explorer } from './explorer'; -import { explorerService } from './explorer_dashboard_service'; -import { getExplorerDefaultAppState, ExplorerAppState } from './reducers'; - -interface ExplorerScope extends IScope { - appState: IAppState; -} - -module.directive('mlAnomalyExplorer', function( - globalState: State, - $rootScope: IRootScopeService, - AppState: AppStateClass -) { - function link($scope: ExplorerScope, element: IRootElementService) { - const subscriptions = new Subscription(); - - const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); - - ReactDOM.render( - - - , - element[0] - ); - - // Initialize the AppState in which to store swimlane and filter settings. - // AppState is used to store state in the URL. - $scope.appState = new AppState(getExplorerDefaultAppState()); - const { mlExplorerFilter, mlExplorerSwimlane } = $scope.appState; - - // Pass the current URL AppState on to anomaly explorer's reactive state. - // After this hand-off, the appState stored in explorerState$ is the single - // source of truth. - explorerService.setAppState({ mlExplorerSwimlane, mlExplorerFilter }); - - // Now that appState in explorerState$ is the single source of truth, - // subscribe to it and update the actual URL appState on changes. - subscriptions.add( - explorerService.appState$.subscribe((appState: ExplorerAppState) => { - $scope.appState.fetch(); - $scope.appState.mlExplorerFilter = appState.mlExplorerFilter; - $scope.appState.mlExplorerSwimlane = appState.mlExplorerSwimlane; - $scope.appState.save(); - $scope.$applyAsync(); - }) - ); - - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => - $rootScope.$applyAsync() - ) - ); - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => - $rootScope.$applyAsync() - ) - ); - subscriptions.add( - subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => - $rootScope.$applyAsync() - ) - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - $scope.$destroy(); - subscriptions.unsubscribe(); - unsubscribeFromGlobalState(); - }); - } - - return { link }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts b/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.ts deleted file mode 100644 index a061176a5ef5b1..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/explorer/explorer_route.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 uiRoutes from 'ui/routes'; - -import '../components/controls'; - -import { checkFullLicense } from '../license/check_license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; -import { mlJobService } from '../services/job_service'; -import { loadIndexPatterns } from '../util/index_utils'; - -import { getAnomalyExplorerBreadcrumbs } from './breadcrumbs'; - -uiRoutes.when('/explorer/?', { - template: ``, - k7Breadcrumbs: getAnomalyExplorerBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPatterns: loadIndexPatterns, - jobs: mlJobService.loadJobsWrapper, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/explorer/index.ts b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts index edc25565daa9f5..1dd9b76b8c20c3 100644 --- a/x-pack/legacy/plugins/ml/public/application/explorer/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/explorer/index.ts @@ -4,9 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../explorer/explorer_dashboard_service'; -import '../explorer/explorer_directive'; -import '../explorer/explorer_route'; -import '../explorer/explorer_charts'; -import '../explorer/select_limit'; -import '../components/job_selector'; +export { Explorer } from './explorer'; diff --git a/x-pack/legacy/plugins/ml/public/application/index.scss b/x-pack/legacy/plugins/ml/public/application/index.scss new file mode 100644 index 00000000000000..ecef2bbf9a5979 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/index.scss @@ -0,0 +1,46 @@ +// Should import both the EUI constants and any Kibana ones that are considered global +@import 'src/legacy/ui/public/styles/styling_constants'; + +// ML has it's own variables for coloring +@import 'variables'; + +// Kibana management page ML section +#kibanaManagementMLSection { + @import 'management/index'; +} + +// Protect the rest of Kibana from ML generic namespacing +// SASSTODO: Prefix ml selectors instead +#ml-app { + // App level + @import 'app'; + + // Sub applications + @import 'data_frame_analytics/index'; + @import 'datavisualizer/index'; + @import 'explorer/index'; // SASSTODO: This file needs to be rewritten + @import 'jobs/index'; // SASSTODO: This collection of sass files has multiple problems + @import 'overview/index'; + @import 'settings/index'; + @import 'timeseriesexplorer/index'; + + // Components + @import 'components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/chart_tooltip/index'; + @import 'components/color_range_legend/index'; + @import 'components/controls/index'; + @import 'components/entity_cell/index'; + @import 'components/field_title_bar/index'; + @import 'components/field_type_icon/index'; + @import 'components/influencers_list/index'; + @import 'components/items_grid/index'; + @import 'components/job_selector/index'; + @import 'components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner + @import 'components/navigation_menu/index'; + @import 'components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly + @import 'components/stats_bar/index'; + + // Hacks are last so they can overwrite anything above if needed + @import 'hacks'; +} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts deleted file mode 100644 index f2954548ea5474..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/breadcrumbs.ts +++ /dev/null @@ -1,112 +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 { i18n } from '@kbn/i18n'; -import { Breadcrumb } from 'ui/chrome'; -import { - ANOMALY_DETECTION_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - ML_BREADCRUMB, -} from '../../breadcrumbs'; - -export function getJobManagementBreadcrumbs(): Breadcrumb[] { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { - defaultMessage: 'Job Management', - }), - href: '', - }, - ]; -} - -export function getCreateJobBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { - defaultMessage: 'Create job', - }), - href: '#/jobs/new_job', - }, - ]; -} - -export function getCreateSingleMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { - defaultMessage: 'Single metric', - }), - href: '', - }, - ]; -} - -export function getCreateMultiMetricJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { - defaultMessage: 'Multi metric', - }), - href: '', - }, - ]; -} - -export function getCreatePopulationJobBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { - defaultMessage: 'Population', - }), - href: '', - }, - ]; -} - -export function getAdvancedJobConfigurationBreadcrumbs(): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { - defaultMessage: 'Advanced configuration', - }), - href: '', - }, - ]; -} - -export function getCreateRecognizerJobBreadcrumbs($routeParams: any): Breadcrumb[] { - return [ - ...getCreateJobBreadcrumbs(), - { - text: $routeParams.id, - href: '', - }, - ]; -} - -export function getDataVisualizerIndexOrSearchBreadcrumbs(): Breadcrumb[] { - return [ - ML_BREADCRUMB, - DATA_VISUALIZER_BREADCRUMB, - { - text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { - defaultMessage: 'Select index or search', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/index.ts new file mode 100644 index 00000000000000..0bb30d4f76de7a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { JobsPage } from './jobs_list'; +export {} from './new_job'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index 5ec407f7f054e6..d78cb371083911 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -7,20 +7,20 @@ import PropTypes from 'prop-types'; import React from 'react'; +import chrome from 'ui/chrome'; import { EuiButtonIcon, EuiToolTip, } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { mlJobService } from '../../../../services/job_service'; import { injectI18n } from '@kbn/i18n/react'; export function getLink(location, jobs) { const resultsPageUrl = mlJobService.createResultsUrlForJobs(jobs, location); - return `${chrome.getBasePath()}/app/${resultsPageUrl}`; + return `${chrome.getBasePath()}/app/ml${resultsPageUrl}`; } function ResultLinksUI({ jobs, intl }) { @@ -39,6 +39,7 @@ function ResultLinksUI({ jobs, intl }) { const singleMetricVisible = (jobs.length < 2); const singleMetricEnabled = (jobs.length === 1 && jobs[0].isSingleMetricViewerJob); const jobActionsDisabled = (jobs.length === 1 && jobs[0].deleting === true); + return ( {(singleMetricVisible) && diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 028e6a10d6abcf..9b301200c76f28 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -6,7 +6,6 @@ import React from 'react'; import { EuiLink } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; @@ -62,7 +61,7 @@ export function extractJobDetails(job) { if (job.calendars) { calendars.items = job.calendars.map(c => [ '', - {c}, + {c}, ]); // remove the calendars list from the general section // so not to show it twice. diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index 3df869174c146b..8ae024e68460a0 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -23,7 +23,6 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { formatDate, formatNumber } from '@elastic/eui/lib/services/format'; -import chrome from 'ui/chrome'; import { FORECAST_REQUEST_STATE } from '../../../../../../../common/constants/states'; import { addItemToRecentlyAccessed } from '../../../../../util/recently_accessed'; @@ -128,7 +127,7 @@ class ForecastsTableUI extends Component { const url = `?_g=${_g}&_a=${_a}`; addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url); - window.open(`${chrome.getBasePath()}/app/ml#/timeseriesexplorer${url}`, '_self'); + window.open(`#/timeseriesexplorer${url}`, '_self'); } render() { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index fc07d4d2a02940..effc54c228130f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -67,15 +67,6 @@ export class JobsListView extends Component { if (this.props.isManagementTable === true) { this.refreshJobSummaryList(true); } else { - // The advanced job wizard is still angularjs based and triggers - // broadcast events which it expects the jobs list to be subscribed to. - this.props.angularWrapperScope.$on('jobsUpdated', () => { - this.refreshJobSummaryList(true); - }); - this.props.angularWrapperScope.$on('openCreateWatchWindow', (e, job) => { - this.showCreateWatchFlyout(job.job_id); - }); - timefilter.disableTimeRangeSelector(); timefilter.enableAutoRefreshSelector(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js deleted file mode 100644 index f549ec3826cb57..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/directive.js +++ /dev/null @@ -1,59 +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 ReactDOM from 'react-dom'; -import React from 'react'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { loadIndexPatterns } from '../../util/index_utils'; -import { checkFullLicense } from '../../license/check_license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; -import { getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; -import { getJobManagementBreadcrumbs } from '../../jobs/breadcrumbs'; -import { loadMlServerInfo } from '../../services/ml_server_info'; - -import uiRoutes from 'ui/routes'; - -const template = ``; - -uiRoutes - .when('/jobs/?', { - template, - k7Breadcrumbs: getJobManagementBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - indexPatterns: loadIndexPatterns, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - } - }); - -import { JobsPage } from './jobs'; -import { I18nContext } from 'ui/i18n'; - -module.directive('jobsPage', function () { - return { - scope: {}, - restrict: 'E', - link: (scope, element) => { - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - } - }; -}); diff --git a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts similarity index 86% rename from x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts index 81e0c494e28b3d..0b70e6b3c93525 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './documentation_links'; +export { JobsPage } from './jobs'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx similarity index 59% rename from x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js rename to x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx index 21c184cdcd298e..f820372e20c09a 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/jobs.tsx @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FC } from 'react'; import { NavigationMenu } from '../../components/navigation_menu'; +// @ts-ignore import { JobsListView } from './components/jobs_list_view'; -export const JobsPage = (props) => ( - <> - - - -); +export const JobsPage: FC<{ props?: any }> = props => { + return ( +
+ + +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts index 22aebc2b88a886..d8917db7a33ffa 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/advanced_job_creator.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, Aggregation, SplitField } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector, CustomRule } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { isValidJson } from '../../../../../../common/util/validation_utils'; import { ml } from '../../../../services/ml_api_service'; @@ -32,7 +32,11 @@ export class AdvancedJobCreator extends JobCreator { private _richDetectors: RichDetector[] = []; private _queryString: string; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this._queryString = JSON.stringify(this._datafeed_config.query); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts index 4960492eabeb33..3246f8ae4b31ac 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/configs/job.ts @@ -5,7 +5,7 @@ */ import { UrlConfig } from '../../../../../../../common/types/custom_urls'; -import { CREATED_BY_LABEL } from '../util/constants'; +import { CREATED_BY_LABEL } from '../../../../../../../common/constants/new_job'; export type JobId = string; export type BucketSpan = string; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index e11cebe0383cd2..4707eff8d844ec 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { UrlConfig } from '../../../../../../common/types/custom_urls'; import { IndexPatternTitle } from '../../../../../../common/types/kibana'; import { ML_JOB_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; @@ -15,7 +15,11 @@ import { Aggregation, Field } from '../../../../../../common/types/fields'; import { createEmptyJob, createEmptyDatafeed } from './util/default_configs'; import { mlJobService } from '../../../../services/job_service'; import { JobRunner, ProgressSubscriber } from '../job_runner'; -import { JOB_TYPE, CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME } from './util/constants'; +import { + JOB_TYPE, + CREATED_BY_LABEL, + SHARED_RESULTS_INDEX_NAME, +} from '../../../../../../common/constants/new_job'; import { isSparseDataJob } from './util/general'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { Calendar } from '../../../../../../common/types/calendars'; @@ -24,7 +28,7 @@ import { mlCalendarService } from '../../../../services/calendar_service'; export class JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; protected _indexPattern: IndexPattern; - protected _savedSearch: SavedSearch; + protected _savedSearch: SavedSearchSavedObject | null; protected _indexPatternTitle: IndexPatternTitle = ''; protected _job_config: Job; protected _calendars: Calendar[]; @@ -44,7 +48,11 @@ export class JobCreator { stop: boolean; } = { stop: false }; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { this._indexPattern = indexPattern; this._savedSearch = savedSearch; this._indexPatternTitle = indexPattern.title; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts index 375e112ed46faf..4ffcd1b06ca479 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator_factory.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { SingleMetricJobCreator } from './single_metric_job_creator'; import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export const jobCreatorFactory = (jobType: JOB_TYPE) => ( indexPattern: IndexPattern, - savedSearch: SavedSearch, + savedSearch: SavedSearchSavedObject | null, query: object ) => { let jc; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index fea328acb58b3c..e86ee09d234f1d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, @@ -15,7 +15,11 @@ import { } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE, CREATED_BY_LABEL, DEFAULT_MODEL_MEMORY_LIMIT } from './util/constants'; +import { + JOB_TYPE, + CREATED_BY_LABEL, + DEFAULT_MODEL_MEMORY_LIMIT, +} from '../../../../../../common/constants/new_job'; import { ml } from '../../../../services/ml_api_service'; import { getRichDetectors } from './util/general'; @@ -26,7 +30,11 @@ export class MultiMetricJobCreator extends JobCreator { private _lastEstimatedModelMemoryLimit = DEFAULT_MODEL_MEMORY_LIMIT; protected _type: JOB_TYPE = JOB_TYPE.MULTI_METRIC; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts index 9e9ccf8ab63e42..8fcd03982424d9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/population_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { JobCreator } from './job_creator'; import { Field, @@ -15,7 +15,7 @@ import { } from '../../../../../../common/types/fields'; import { Job, Datafeed, Detector } from './configs'; import { createBasicDetector } from './util/default_configs'; -import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; export class PopulationJobCreator extends JobCreator { @@ -25,7 +25,11 @@ export class PopulationJobCreator extends JobCreator { private _byFields: SplitField[] = []; protected _type: JOB_TYPE = JOB_TYPE.POPULATION; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.POPULATION; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index 5f3f6ff310d289..cb8a46ade513cb 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; import { IndexPattern } from 'ui/index_patterns'; +import { SavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { JobCreator } from './job_creator'; import { Field, Aggregation, AggFieldPair } from '../../../../../../common/types/fields'; @@ -15,13 +15,17 @@ import { ML_JOB_AGGREGATION, ES_AGGREGATION, } from '../../../../../../common/constants/aggregation_types'; -import { JOB_TYPE, CREATED_BY_LABEL } from './util/constants'; +import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; export class SingleMetricJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; - constructor(indexPattern: IndexPattern, savedSearch: SavedSearch, query: object) { + constructor( + indexPattern: IndexPattern, + savedSearch: SavedSearchSavedObject | null, + query: object + ) { super(indexPattern, savedSearch, query); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts index 7162ec65767f9f..9feb0416dd267f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/type_guards.ts @@ -8,7 +8,7 @@ import { SingleMetricJobCreator } from './single_metric_job_creator'; import { MultiMetricJobCreator } from './multi_metric_job_creator'; import { PopulationJobCreator } from './population_job_creator'; import { AdvancedJobCreator } from './advanced_job_creator'; -import { JOB_TYPE } from './util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; export type JobCreatorType = | SingleMetricJobCreator diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index e7e5e8aa64f7bf..760dbe447dc894 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -19,7 +19,7 @@ import { } from '../../../../../../../common/types/fields'; import { mlJobService } from '../../../../../services/job_service'; import { JobCreatorType, isMultiMetricJobCreator, isPopulationJobCreator } from '../index'; -import { CREATED_BY_LABEL, JOB_TYPE } from './constants'; +import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../../common/constants/new_job'; const getFieldByIdFactory = (scriptFields: Field[]) => (id: string) => { let field = newJobCapsService.getFieldById(id); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 82808ef3d37ee0..5048f44586a386 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -12,8 +12,8 @@ import { getSeverityType } from '../../../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { ANOMALY_SEVERITY } from '../../../../../../common/constants/anomalies'; import { getScoresByRecord } from './searches'; -import { JOB_TYPE } from '../job_creator/util/constants'; import { ChartLoader } from '../chart_loader'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; import { ES_AGGREGATION } from '../../../../../../common/constants/aggregation_types'; export interface Results { diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts deleted file mode 100644 index 945d22967a65d6..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/index.ts +++ /dev/null @@ -1,14 +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 './pages/new_job/route'; -import './pages/new_job/directive'; -import './pages/job_type/route'; -import './pages/job_type/directive'; -import './pages/index_or_search/route'; -import './pages/index_or_search/directive'; -import './recognize/route'; -import './recognize/directive'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx index 097050fd829c98..0e6dd81fb91a98 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/query_delay/query_delay_input.tsx @@ -9,7 +9,7 @@ import { EuiFieldText } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { Description } from './description'; import { useStringifiedValue } from '../hooks'; -import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; +import { DEFAULT_QUERY_DELAY } from '../../../../../../../../../common/constants/new_job'; export const QueryDelayInput: FC = () => { const { jobCreator, jobCreatorUpdate, jobValidator, jobValidatorUpdated } = useContext( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx index 87e133df225a0b..5fbc7557a2fa79 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/advanced_section/advanced_section.tsx @@ -17,7 +17,7 @@ import { ModelPlotSwitch } from './components/model_plot'; import { DedicatedIndexSwitch } from './components/dedicated_index'; import { ModelMemoryLimitInput } from '../../../common/model_memory_limit'; import { JobCreatorContext } from '../../../job_creator_context'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; const ButtonContent = i18n.translate( 'xpack.ml.newJob.wizard.jobDetailsStep.advancedSectionButton', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx index b76fc120538f5d..f11cdc62337172 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/chart_grid.tsx @@ -11,7 +11,7 @@ import { AggFieldPair, SplitField } from '../../../../../../../../../common/type import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SplitCards, useAnimateSplit } from '../split_cards'; import { DetectorTitle } from '../detector_title'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx index 8cd533f8b2e297..035e3c90f53ae2 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/chart_grid.tsx @@ -11,7 +11,7 @@ import { AggFieldPair, SplitField } from '../../../../../../../../../common/type import { ChartSettings } from '../../../charts/common/settings'; import { LineChartData } from '../../../../../common/chart_loader'; import { ModelItem, Anomaly } from '../../../../../common/results_loader'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SplitCards, useAnimateSplit } from '../split_cards'; import { DetectorTitle } from '../detector_title'; import { ByFieldSelector } from '../split_field'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx index 918163572076ce..118923aa203e17 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_cards/split_cards.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { SplitField } from '../../../../../../../../../common/types/fields'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; interface Props { fieldValues: string[]; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx index 2c601307397800..6d4eeff2a54752 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field/description.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; interface Props { jobType: JOB_TYPE; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx index bdb2076086fd56..795dfc30f954a4 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/pick_fields.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { JobCreatorContext } from '../job_creator_context'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { SingleMetricView } from './components/single_metric_view'; import { MultiMetricView } from './components/multi_metric_view'; import { PopulationView } from './components/population_view'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx index c624972aa07ea7..d1900413d84c9d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/datafeed_details/datafeed_details.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiDescriptionList, EuiFormRow } from '@elas import { JobCreatorContext } from '../../../job_creator_context'; import { MLJobEditor } from '../../../../../../jobs_list/components/ml_job_editor'; import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../../../common/util/job_utils'; -import { DEFAULT_QUERY_DELAY } from '../../../../../common/job_creator/util/constants'; +import { DEFAULT_QUERY_DELAY } from '../../../../../../../../../common/constants/new_job'; import { getNewJobDefaults } from '../../../../../../../services/ml_server_info'; import { ListItems, defaultLabel, Italic } from '../common'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx index bf60eda2e81c35..f72ff6cf985e56 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/detector_chart/detector_chart.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC, useContext } from 'react'; import { JobCreatorContext } from '../../../job_creator_context'; -import { JOB_TYPE } from '../../../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../../../common/constants/new_job'; import { SingleMetricView } from '../../../pick_fields_step/components/single_metric_view'; import { MultiMetricView } from '../../../pick_fields_step/components/multi_metric_view'; import { PopulationView } from '../../../pick_fields_step/components/population_view'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 3f241f21a75e53..994847864d6bb6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -23,7 +23,7 @@ import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; import { DatafeedDetails } from './components/datafeed_details'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx index 7410e10aa92cfd..70a529b8e24d0f 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/time_range_step/time_range.tsx @@ -5,6 +5,8 @@ */ import React, { FC, Fragment, useContext, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { toastNotifications } from 'ui/notify'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { timefilter } from 'ui/timefilter'; @@ -16,7 +18,7 @@ import { useKibanaContext } from '../../../../../contexts/kibana'; import { FullTimeRangeSelector } from '../../../../../components/full_time_range_selector'; import { EventRateChart } from '../charts/event_rate_chart'; import { LineChartPoint } from '../../../common/chart_loader'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; import { GetTimeFieldRangeResponse } from '../../../../../services/ml_api_service'; import { TimeRangePicker, TimeRange } from '../../../common/components'; @@ -78,10 +80,18 @@ export const TimeRangeStep: FC = ({ setCurrentStep, isCurrentStep }) }, [jobCreatorUpdated]); function fullTimeRangeCallback(range: GetTimeFieldRangeResponse) { - setTimeRange({ - start: range.start.epoch, - end: range.end.epoch, - }); + if (range.start.epoch !== null && range.end.epoch !== null) { + setTimeRange({ + start: range.start.epoch, + end: range.end.epoch, + }); + } else { + toastNotifications.addDanger( + i18n.translate('xpack.ml.newJob.wizard.timeRangeStep.fullTimeRangeError', { + defaultMessage: 'An error occurred obtaining the time range for the index', + }) + ); + } } return ( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index a543dbaaf3c5d1..19b89ffec02ac9 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -10,7 +10,7 @@ import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { mlJobService } from '../../../../../services/job_service'; import { ValidateJob } from '../../../../../components/validate_job'; -import { JOB_TYPE } from '../../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; const idFilterList = [ 'job_id_valid', diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js deleted file mode 100644 index ffa16930e79f20..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../../util/index_utils'; - -describe('ML - Index or Saved Search selection directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Index or Saved Search selection directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx deleted file mode 100644 index 9bd653708d9c01..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/directive.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../../common/types/angular'; -import { Page } from './page'; - -module.directive('mlIndexOrSearch', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const $route = $injector.get('$route'); - const { nextStepPath } = $route.current.locals; - - ReactDOM.render( - {React.createElement(Page, { nextStepPath })}, - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/graph/server/routes/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts similarity index 70% rename from x-pack/legacy/plugins/graph/server/routes/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts index 6201e885fe59c7..31e0f67c0a0faa 100644 --- a/x-pack/legacy/plugins/graph/server/routes/index.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { graphExploreRoute } from './graph_explore'; -export { searchProxyRoute } from './search_proxy'; +export { Page } from './page'; +export { preConfiguredJobRedirect } from './preconfigured_job_redirect'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 0265129d9ccab6..8500279e742b78 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; import { mlJobService } from '../../../../services/job_service'; import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils'; import { CombinedJob } from '../../common/job_creator/configs'; -import { CREATED_BY_LABEL, JOB_TYPE } from '../../common/job_creator/util/constants'; +import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; -export async function preConfiguredJobRedirect() { +export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) { const { job } = mlJobService.tempJobCloningObjects; if (job) { try { - await loadIndexPatterns(); + await loadIndexPatterns(indexPatterns); const redirectUrl = getWizardUrlFromCloningJob(job); window.location.href = `#/${redirectUrl}`; return Promise.reject(); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts deleted file mode 100644 index 6dd5df177bd14c..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/index_or_search/route.ts +++ /dev/null @@ -1,47 +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 uiRoutes from 'ui/routes'; -import { checkMlNodesAvailable } from '../../../../ml_nodes_check'; -import { preConfiguredJobRedirect } from './preconfigured_job_redirect'; -import { checkLicenseExpired, checkBasicLicense } from '../../../../license/check_license'; -import { loadIndexPatterns } from '../../../../util/index_utils'; -import { - checkCreateJobsPrivilege, - checkFindFileStructurePrivilege, -} from '../../../../privilege/check_privilege'; -import { - getCreateJobBreadcrumbs, - getDataVisualizerIndexOrSearchBreadcrumbs, -} from '../../../breadcrumbs'; - -uiRoutes.when('/jobs/new_job', { - redirectTo: '/jobs/new_job/step/index_or_search', -}); - -uiRoutes.when('/jobs/new_job/step/index_or_search', { - template: '', - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPatterns: loadIndexPatterns, - preConfiguredJobRedirect, - checkMlNodesAvailable, - nextStepPath: () => '#/jobs/new_job/step/job_type', - }, -}); - -uiRoutes.when('/datavisualizer_index_select', { - template: '', - k7Breadcrumbs: getDataVisualizerIndexOrSearchBreadcrumbs, - resolve: { - CheckLicense: checkBasicLicense, - privileges: checkFindFileStructurePrivilege, - indexPatterns: loadIndexPatterns, - nextStepPath: () => '#jobs/new_job/datavisualizer', - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js deleted file mode 100644 index bdf65e3bafe962..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../../util/index_utils'; - -describe('ML - Job Type Directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Job Type Directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx deleted file mode 100644 index 3f2a7e553c7e0a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/directive.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../../common/types/angular'; -import { createSearchItems } from '../../utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; -import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; - -module.directive('mlJobTypePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/index.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts similarity index 84% rename from x-pack/legacy/plugins/ml/public/application/jobs/index.js rename to x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts index 1ade8752d6721d..7e2d651439ae30 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/index.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ - - -import './jobs_list'; -import './new_job'; +export { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index 4991039ffa2886..dbae1948cbe0f6 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useKibanaContext } from '../../../../contexts/kibana'; +import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; import { timeBasedIndexCheck } from '../../../../util/index_utils'; @@ -32,33 +33,33 @@ export const Page: FC = () => { const isTimeBasedIndex = timeBasedIndexCheck(currentIndexPattern); const indexWarningTitle = - !isTimeBasedIndex && currentSavedSearch.id === undefined - ? i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { - defaultMessage: 'Index pattern {indexPatternTitle} is not time based', - values: { indexPatternTitle: currentIndexPattern.title }, - }) - : i18n.translate( + !isTimeBasedIndex && isSavedSearchSavedObject(currentSavedSearch) + ? i18n.translate( 'xpack.ml.newJob.wizard.jobType.indexPatternFromSavedSearchNotTimeBasedMessage', { defaultMessage: '{savedSearchTitle} uses index pattern {indexPatternTitle} which is not time based', values: { - savedSearchTitle: currentSavedSearch.title, + savedSearchTitle: currentSavedSearch.attributes.title as string, indexPatternTitle: currentIndexPattern.title, }, } - ); - const pageTitleLabel = - currentSavedSearch.id !== undefined - ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { - defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: currentSavedSearch.title }, - }) - : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { - defaultMessage: 'index pattern {indexPatternTitle}', + ) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternNotTimeBasedMessage', { + defaultMessage: 'Index pattern {indexPatternTitle} is not time based', values: { indexPatternTitle: currentIndexPattern.title }, }); + const pageTitleLabel = isSavedSearchSavedObject(currentSavedSearch) + ? i18n.translate('xpack.ml.newJob.wizard.jobType.savedSearchPageTitleLabel', { + defaultMessage: 'saved search {savedSearchTitle}', + values: { savedSearchTitle: currentSavedSearch.attributes.title as string }, + }) + : i18n.translate('xpack.ml.newJob.wizard.jobType.indexPatternPageTitleLabel', { + defaultMessage: 'index pattern {indexPatternTitle}', + values: { indexPatternTitle: currentIndexPattern.title }, + }); + const recognizerResults = { count: 0, onChange() { @@ -67,14 +68,15 @@ export const Page: FC = () => { }; const getUrl = (basePath: string) => { - return currentSavedSearch.id === undefined + return !isSavedSearchSavedObject(currentSavedSearch) ? `${basePath}?index=${currentIndexPattern.id}` : `${basePath}?savedSearchId=${currentSavedSearch.id}`; }; const addSelectionToRecentlyAccessed = () => { - const title = - currentSavedSearch.id === undefined ? currentIndexPattern.title : currentSavedSearch.title; + const title = !isSavedSearchSavedObject(currentSavedSearch) + ? currentIndexPattern.title + : (currentSavedSearch.attributes.title as string); const url = getUrl(''); addItemToRecentlyAccessed('jobs/new_job/datavisualizer', title, url); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts deleted file mode 100644 index ac2c838dbed311..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/job_type/route.ts +++ /dev/null @@ -1,25 +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 uiRoutes from 'ui/routes'; - -import { checkMlNodesAvailable } from '../../../../ml_nodes_check'; -import { checkLicenseExpired } from '../../../../license/check_license'; -import { checkCreateJobsPrivilege } from '../../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; -import { getCreateJobBreadcrumbs } from '../../../breadcrumbs'; - -uiRoutes.when('/jobs/new_job/step/job_type', { - template: '', - k7Breadcrumbs: getCreateJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx deleted file mode 100644 index d152dfc488ff86..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/directive.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; -import { I18nContext } from 'ui/i18n'; -import { IndexPatternsContract } from '../../../../../../../../../../src/plugins/data/public'; - -import { InjectorService } from '../../../../../../common/types/angular'; -import { createSearchItems } from '../../utils/new_job_utils'; -import { Page, PageProps } from './page'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../../contexts/kibana'; - -module.directive('mlNewJobPage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - const existingJobsAndGroups = $route.current.locals.existingJobsAndGroups; - - if ($route.current.locals.jobType === undefined) { - return; - } - const jobType: JOB_TYPE = $route.current.locals.jobType; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - const props: PageProps = { - existingJobsAndGroups, - jobType, - }; - - ReactDOM.render( - - - {React.createElement(Page, props)} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/index.ts new file mode 100644 index 00000000000000..7e2d651439ae30 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/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 { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index e086b2b8aad7f0..79f98c1170ff8d 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -16,7 +16,7 @@ import { JOB_TYPE, DEFAULT_MODEL_MEMORY_LIMIT, DEFAULT_BUCKET_SPAN, -} from '../../common/job_creator/util/constants'; +} from '../../../../../../common/constants/new_job'; import { ChartLoader } from '../../common/chart_loader'; import { ResultsLoader } from '../../common/results_loader'; import { JobValidator } from '../../common/job_validator'; @@ -104,7 +104,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { jobCreator.modelPlot = true; } - if (kibanaContext.currentSavedSearch.id !== undefined) { + if (kibanaContext.currentSavedSearch !== null) { // Jobs created from saved searches cannot be cloned in the wizard as the // ML job config holds no reference to the saved search ID. jobCreator.createdBy = null; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts deleted file mode 100644 index a527d92342d4ce..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/route.ts +++ /dev/null @@ -1,65 +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 uiRoutes from 'ui/routes'; - -import { checkFullLicense } from '../../../../license/check_license'; -import { checkGetJobsPrivilege } from '../../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../../util/index_utils'; - -import { - getCreateSingleMetricJobBreadcrumbs, - getCreateMultiMetricJobBreadcrumbs, - getCreatePopulationJobBreadcrumbs, - getAdvancedJobConfigurationBreadcrumbs, -} from '../../../breadcrumbs'; - -import { Route } from '../../../../../../common/types/kibana'; - -import { loadNewJobCapabilities } from '../../../../services/new_job_capabilities_service'; - -import { loadMlServerInfo } from '../../../../services/ml_server_info'; - -import { mlJobService } from '../../../../services/job_service'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; - -const template = ``; - -const routes: Route[] = [ - { - id: JOB_TYPE.SINGLE_METRIC, - k7Breadcrumbs: getCreateSingleMetricJobBreadcrumbs, - }, - { - id: JOB_TYPE.MULTI_METRIC, - k7Breadcrumbs: getCreateMultiMetricJobBreadcrumbs, - }, - { - id: JOB_TYPE.POPULATION, - k7Breadcrumbs: getCreatePopulationJobBreadcrumbs, - }, - { - id: JOB_TYPE.ADVANCED, - k7Breadcrumbs: getAdvancedJobConfigurationBreadcrumbs, - }, -]; - -routes.forEach((route: Route) => { - uiRoutes.when(`/jobs/new_job/${route.id}`, { - template, - k7Breadcrumbs: route.k7Breadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - loadNewJobCapabilities, - loadMlServerInfo, - existingJobsAndGroups: mlJobService.getJobAndGroupIds, - jobType: () => route.id, - }, - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx index 50b8650f99bb89..b63ada4bb535c3 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard.tsx @@ -20,7 +20,7 @@ import { JobValidator } from '../../common/job_validator'; import { newJobCapsService } from '../../../../services/new_job_capabilities_service'; import { WizardSteps } from './wizard_steps'; import { WizardHorizontalSteps } from './wizard_horizontal_steps'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; interface Props { jobCreator: JobCreatorType; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx index b5369402230b72..18b199ca8983fc 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_horizontal_steps.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiStepsHorizontal } from '@elastic/eui'; import { WIZARD_STEPS } from '../components/step_types'; -import { JOB_TYPE } from '../../common/job_creator/util/constants'; +import { JOB_TYPE } from '../../../../../../common/constants/new_job'; interface Props { currentStep: WIZARD_STEPS; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx index 0f4eae230acfd8..8e81c05092c98b 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/pages/new_job/wizard_steps.tsx @@ -34,10 +34,10 @@ export const WizardSteps: FC = ({ currentStep, setCurrentStep }) => { const [additionalExpanded, setAdditionalExpanded] = useState(false); function getSummaryStepTitle() { - if (kibanaContext.currentSavedSearch.id !== undefined) { + if (kibanaContext.currentSavedSearch !== null) { return i18n.translate('xpack.ml.newJob.wizard.stepComponentWrapper.summaryTitleSavedSearch', { defaultMessage: 'New job from saved search {title}', - values: { title: kibanaContext.currentSavedSearch.title }, + values: { title: kibanaContext.currentSavedSearch.attributes.title as string }, }); } else if (kibanaContext.currentIndexPattern.id !== undefined) { return i18n.translate( diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js deleted file mode 100644 index d5d5ee4438e329..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/__test__/directive.js +++ /dev/null @@ -1,45 +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 ngMock from 'ng_mock'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -// Import this way to be able to stub/mock functions later on in the tests using sinon. -import * as indexUtils from '../../../../util/index_utils'; - -describe('ML - Recognize job directive', () => { - let $scope; - let $compile; - let $element; - - beforeEach(ngMock.module('kibana')); - beforeEach(() => { - ngMock.inject(function ($injector) { - $compile = $injector.get('$compile'); - const $rootScope = $injector.get('$rootScope'); - $scope = $rootScope.$new(); - }); - }); - - afterEach(() => { - $scope.$destroy(); - }); - - it('Initialize Recognize job directive', done => { - sinon.stub(indexUtils, 'timeBasedIndexCheck').callsFake(() => false); - ngMock.inject(function () { - expect(() => { - $element = $compile('')($scope); - }).to.not.throwError(); - - // directive has scope: false - const scope = $element.isolateScope(); - expect(scope).to.eql(undefined); - done(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx deleted file mode 100644 index 4ed12dfff4c207..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/directive.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); -import { timefilter } from 'ui/timefilter'; - -import { I18nContext } from 'ui/i18n'; -import { InjectorService } from '../../../../../common/types/angular'; - -import { createSearchItems } from '../utils/new_job_utils'; -import { Page } from './page'; - -import { KibanaContext, KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; - -module.directive('mlRecognizePage', ($injector: InjectorService) => { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - // remove time picker from top of page - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - const indexPatterns = $injector.get('indexPatterns'); - const kibanaConfig = $injector.get('config'); - const $route = $injector.get('$route'); - - const moduleId = $route.current.params.id; - const existingGroupIds: string[] = $route.current.locals.existingJobsAndGroups.groupIds; - - const { indexPattern, savedSearch, combinedQuery } = createSearchItems( - kibanaConfig, - $route.current.locals.indexPattern, - $route.current.locals.savedSearch - ); - const kibanaContext = { - combinedQuery, - currentIndexPattern: indexPattern, - currentSavedSearch: savedSearch, - indexPatterns, - kibanaConfig, - }; - - ReactDOM.render( - - - {React.createElement(Page, { moduleId, existingGroupIds })} - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/index.ts new file mode 100644 index 00000000000000..7e2d651439ae30 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/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 { Page } from './page'; diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 11b2a8f01342db..141ed5d1bbb8ff 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -85,17 +85,17 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { combinedQuery, } = useKibanaContext(); const pageTitle = - savedSearch.id !== undefined + savedSearch !== null ? i18n.translate('xpack.ml.newJob.recognize.savedSearchPageTitle', { defaultMessage: 'saved search {savedSearchTitle}', - values: { savedSearchTitle: savedSearch.title }, + values: { savedSearchTitle: savedSearch.attributes.title as string }, }) : i18n.translate('xpack.ml.newJob.recognize.indexPatternPageTitle', { defaultMessage: 'index pattern {indexPatternTitle}', values: { indexPatternTitle: indexPattern.title }, }); - const displayQueryWarning = savedSearch.id !== undefined; - const tempQuery = savedSearch.id === undefined ? undefined : combinedQuery; + const displayQueryWarning = savedSearch !== null; + const tempQuery = savedSearch === null ? undefined : combinedQuery; /** * Loads recognizer module configuration. diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index d2ca22972c2019..cb44210b970e7e 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -16,25 +16,20 @@ import { KibanaObjects } from './page'; * Redirects to the Anomaly Explorer to view the jobs if they have been created, * or the recognizer job wizard for the module if not. */ -export function checkViewOrCreateJobs($route: any) { +export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise { return new Promise((resolve, reject) => { - const moduleId = $route.current.params.id; - const indexPatternId = $route.current.params.index; - // Load the module, and check if the job(s) in the module have been created. // If so, load the jobs in the Anomaly Explorer. // Otherwise open the data recognizer wizard for the module. // Always want to call reject() so as not to load original page. ml.dataRecognizerModuleJobsExist({ moduleId }) .then((resp: any) => { - const basePath = `${chrome.getBasePath()}/app/`; - if (resp.jobsExist === true) { const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = `${basePath}${resultsPageUrl}`; + window.location.href = resultsPageUrl; reject(); } else { - window.location.href = `${basePath}ml#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; reject(); } }) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts deleted file mode 100644 index 7b1d71540c163c..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/route.ts +++ /dev/null @@ -1,34 +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 uiRoutes from 'ui/routes'; -import { checkMlNodesAvailable } from '../../..//ml_nodes_check/check_ml_nodes'; -import { checkLicenseExpired } from '../../..//license/check_license'; -import { getCreateRecognizerJobBreadcrumbs } from '../../breadcrumbs'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; -import { loadCurrentIndexPattern, loadCurrentSavedSearch } from '../../../util/index_utils'; -import { mlJobService } from '../../../services/job_service'; -import { checkViewOrCreateJobs } from './resolvers'; - -uiRoutes.when('/jobs/new_job/recognize', { - template: '', - k7Breadcrumbs: getCreateRecognizerJobBreadcrumbs, - resolve: { - CheckLicense: checkLicenseExpired, - privileges: checkCreateJobsPrivilege, - indexPattern: loadCurrentIndexPattern, - savedSearch: loadCurrentSavedSearch, - checkMlNodesAvailable, - existingJobsAndGroups: mlJobService.getJobAndGroupIds, - }, -}); - -uiRoutes.when('/modules/check_view_or_create', { - template: '', - resolve: { - checkViewOrCreateJobs, - }, -}); diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts index 455fac9b532d61..050387e6de263c 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/utils/new_job_utils.ts @@ -5,24 +5,18 @@ */ import { IndexPattern } from 'ui/index_patterns'; -import { SavedSearch } from 'src/legacy/core_plugins/kibana/public/discover/types'; +import { esQuery, Query, esKuery } from '../../../../../../../../../src/plugins/data/public'; import { KibanaConfigTypeFix } from '../../../contexts/kibana'; -import { esQuery, Query, IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; - -export interface SearchItems { - indexPattern: IIndexPattern; - savedSearch: SavedSearch; - query: any; - combinedQuery: any; -} +import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; +import { SavedSearchSavedObject } from '../../../../../common/types/kibana'; +import { getQueryFromSavedSearch } from '../../../util/index_utils'; // Provider for creating the items used for searching and job creation. -// Uses the $route object to retrieve the indexPattern and savedSearch from the url export function createSearchItems( kibanaConfig: KibanaConfigTypeFix, indexPattern: IndexPattern, - savedSearch: SavedSearch + savedSearch: SavedSearchSavedObject | null ) { // query is only used by the data visualizer as it needs // a lucene query_string. @@ -43,22 +37,36 @@ export function createSearchItems( }, }; - if (indexPattern.id === undefined && savedSearch.id !== undefined) { - const searchSource = savedSearch.searchSource; - indexPattern = searchSource.getField('index')!; + if (savedSearch !== null) { + const data = getQueryFromSavedSearch(savedSearch); - query = searchSource.getField('query')!; - const fs = searchSource.getField('filter'); + query = data.query; + const filter = data.filter; - const filters = Array.isArray(fs) ? fs : []; + const filters = Array.isArray(filter) ? filter : []; - const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); - combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); + if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) { + const ast = esKuery.fromKueryExpression(query.query); + if (query.query !== '') { + combinedQuery = esKuery.toElasticsearchQuery(ast, indexPattern); + } + const filterQuery = esQuery.buildQueryFromFilters(filters, indexPattern); + + if (combinedQuery.bool.filter === undefined) { + combinedQuery.bool.filter = []; + } + if (combinedQuery.bool.must_not === undefined) { + combinedQuery.bool.must_not = []; + } + combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter]; + combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not]; + } else { + const esQueryConfigs = esQuery.getEsQueryConfig(kibanaConfig); + combinedQuery = esQuery.buildEsQuery(indexPattern, [query], filters, esQueryConfigs); + } } return { - indexPattern, - savedSearch, query, combinedQuery, }; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts deleted file mode 100644 index 9df503b462b6c0..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/overview/breadcrumbs.ts +++ /dev/null @@ -1,23 +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 { i18n } from '@kbn/i18n'; -// @ts-ignore -import { ML_BREADCRUMB } from '../../breadcrumbs'; - -export function getOverviewBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - { - text: i18n.translate('xpack.ml.overviewBreadcrumbs.overviewLabel', { - defaultMessage: 'Overview', - }), - href: '', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx b/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx deleted file mode 100644 index bd3b653ccbb64a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/overview/directive.tsx +++ /dev/null @@ -1,38 +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 ReactDOM from 'react-dom'; -import React from 'react'; -import { I18nContext } from 'ui/i18n'; -import { timefilter } from 'ui/timefilter'; -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import { OverviewPage } from './overview_page'; - -module.directive('mlOverview', function() { - return { - scope: {}, - restrict: 'E', - link: async (scope: ng.IScope, element: ng.IAugmentedJQuery) => { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - ReactDOM.render( - - - , - element[0] - ); - - element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode(element[0]); - scope.$destroy(); - }); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/overview/index.ts b/x-pack/legacy/plugins/ml/public/application/overview/index.ts index ac00eab1f2cdb0..7d99bb10940154 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/overview/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './route'; -import './directive'; +export { OverviewPage } from './overview_page'; diff --git a/x-pack/legacy/plugins/ml/public/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts similarity index 64% rename from x-pack/legacy/plugins/ml/public/breadcrumbs.ts rename to x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts index ba4703d4818ff5..6d8138d4bcd2c6 100644 --- a/x-pack/legacy/plugins/ml/public/breadcrumbs.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/breadcrumbs.ts @@ -5,31 +5,32 @@ */ import { i18n } from '@kbn/i18n'; +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -export const ML_BREADCRUMB = Object.freeze({ +export const ML_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.machineLearningBreadcrumbLabel', { defaultMessage: 'Machine Learning', }), href: '#/', }); -export const SETTINGS = Object.freeze({ +export const SETTINGS: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.settingsBreadcrumbLabel', { defaultMessage: 'Settings', }), - href: '#/settings?', + href: '#/settings', }); -export const ANOMALY_DETECTION_BREADCRUMB = Object.freeze({ +export const ANOMALY_DETECTION_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.anomalyDetectionBreadcrumbLabel', { defaultMessage: 'Anomaly Detection', }), - href: '#/jobs?', + href: '#/jobs', }); -export const DATA_VISUALIZER_BREADCRUMB = Object.freeze({ +export const DATA_VISUALIZER_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ text: i18n.translate('xpack.ml.datavisualizerBreadcrumbLabel', { defaultMessage: 'Data Visualizer', }), - href: '#/datavisualizer?', + href: '#/datavisualizer', }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/index.ts new file mode 100644 index 00000000000000..3ec5361568526d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/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 { MlRouter, MlRoute } from './router'; diff --git a/x-pack/legacy/plugins/ml/public/application/overview/route.ts b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts similarity index 50% rename from x-pack/legacy/plugins/ml/public/application/overview/route.ts rename to x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts index e737961e184fce..30c5fbc497afe6 100644 --- a/x-pack/legacy/plugins/ml/public/application/overview/route.ts +++ b/x-pack/legacy/plugins/ml/public/application/routing/resolvers.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import uiRoutes from 'ui/routes'; -import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; +import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license/check_license'; import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; -import { getOverviewBreadcrumbs } from './breadcrumbs'; -import './directive'; - -const template = ``; +import { PageDependencies } from './router'; -uiRoutes.when('/overview/?', { - template, - k7Breadcrumbs: getOverviewBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - loadMlServerInfo, - }, +export interface Resolvers { + [name: string]: () => Promise; +} +export interface ResolverResults { + [name: string]: any; +} +export const basicResolvers = (deps: PageDependencies): Resolvers => ({ + checkFullLicense, + getMlNodeCount, + loadMlServerInfo, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + loadSavedSearches, }); diff --git a/x-pack/legacy/plugins/ml/public/application/routing/router.tsx b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx new file mode 100644 index 00000000000000..174c1ef1d4fe8d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/router.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { HashRouter, Route, RouteProps } from 'react-router-dom'; +import { Location } from 'history'; +import { I18nContext } from 'ui/i18n'; + +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { KibanaContext, KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; + +import * as routes from './routes'; + +// custom RouteProps making location non-optional +interface MlRouteProps extends RouteProps { + location: Location; +} + +export interface MlRoute { + path: string; + render(props: MlRouteProps, config: KibanaConfigTypeFix, deps: PageDependencies): JSX.Element; + breadcrumbs: ChromeBreadcrumb[]; +} + +export interface PageProps { + location: Location; + config: KibanaConfigTypeFix; + deps: PageDependencies; +} + +export interface PageDependencies { + indexPatterns: IndexPatternsContract; +} + +export const PageLoader: FC<{ context: KibanaContextValue }> = ({ context, children }) => { + return context === null ? null : ( + + {children} + + ); +}; + +export const MlRouter: FC<{ + config: KibanaConfigTypeFix; + setBreadcrumbs: (breadcrumbs: ChromeBreadcrumb[]) => void; + indexPatterns: IndexPatternsContract; +}> = ({ config, setBreadcrumbs, indexPatterns }) => { + return ( + +
+ {Object.entries(routes).map(([name, route]) => ( + { + window.setTimeout(() => { + setBreadcrumbs(route.breadcrumbs); + }); + return route.render(props, config, { indexPatterns }); + }} + /> + ))} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx new file mode 100644 index 00000000000000..3a2f445ac6b82c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/access_denied.tsx @@ -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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { Page } from '../../access_denied'; + +const breadcrumbs = [ + { + text: i18n.translate('xpack.ml.accessDeniedLabel', { + defaultMessage: 'Access denied', + }), + href: '', + }, +]; + +export const accessDeniedRoute: MlRoute = { + path: '/access-denied', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, {}); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx new file mode 100644 index 00000000000000..41c286c54836c2 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -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 React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; +import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; +import { DATA_FRAME_TASK_STATE } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '', + }, +]; + +export const analyticsJobExplorationRoute: MlRoute = { + path: '/data_frame_analytics/exploration', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, basicResolvers(deps)); + const { _g } = queryString.parse(location.search); + let globalState: any = null; + try { + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global state'); + window.location.href = '#data_frame_analytics'; + } + const jobId: string = globalState.ml.jobId; + const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; + const jobStatus: DATA_FRAME_TASK_STATE = globalState.ml.jobStatus; + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx new file mode 100644 index 00000000000000..31bd10f2138ad1 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -0,0 +1,39 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../data_frame_analytics/pages/analytics_management'; +import { ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { + defaultMessage: 'Data Frame Analytics', + }), + href: '', + }, +]; + +export const analyticsJobsListRoute: MlRoute = { + path: '/data_frame_analytics', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, basicResolvers(deps)); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts new file mode 100644 index 00000000000000..552c15a408b65b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/data_frame_analytics/index.ts @@ -0,0 +1,8 @@ +/* + * 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 * from './analytics_jobs_list'; +export * from './analytics_job_exploration'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx new file mode 100644 index 00000000000000..3faca285319d5f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * 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, { FC } from 'react'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { DatavisualizerSelector } from '../../../datavisualizer'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; + +export const selectorRoute: MlRoute = { + path: '/datavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkBasicLicense, + checkFindFileStructurePrivilege, + }); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx new file mode 100644 index 00000000000000..11e6b85f939d3f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -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. + */ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { loadIndexPatterns } from '../../../util/index_utils'; + +import { getMlNodeCount } from '../../../ml_nodes_check'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { + defaultMessage: 'File', + }), + href: '', + }, +]; + +export const fileBasedRoute: MlRoute = { + path: '/filedatavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkFindFileStructurePrivilege, + getMlNodeCount, + }); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts new file mode 100644 index 00000000000000..7f61317ef34021 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './datavisualizer'; +export * from './index_based'; +export * from './file_based'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx new file mode 100644 index 00000000000000..ab359238695d42 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -0,0 +1,53 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { Page } from '../../../datavisualizer/index_based'; + +import { checkBasicLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { loadIndexPatterns } from '../../../util/index_utils'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check'; +import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + DATA_VISUALIZER_BREADCRUMB, + { + text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { + defaultMessage: 'Index', + }), + href: '', + }, +]; + +export const indexBasedRoute: MlRoute = { + path: '/jobs/new_job/datavisualizer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context } = useResolver(index, savedSearchId, config, { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx new file mode 100644 index 00000000000000..1b6b91026d6a5a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/explorer.tsx @@ -0,0 +1,156 @@ +/* + * 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, { FC, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; +import { Subscription } from 'rxjs'; + +// @ts-ignore +import queryString from 'query-string'; +import { timefilter } from 'ui/timefilter'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { Explorer } from '../../explorer'; +import { mlJobService } from '../../services/job_service'; +import { getExplorerDefaultAppState, ExplorerAppState } from '../../explorer/reducers'; +import { explorerService } from '../../explorer/explorer_dashboard_service'; +import { jobSelectServiceFactory } from '../../components/job_selector/job_select_service_utils'; +import { subscribeAppStateToObservable } from '../../util/app_state_utils'; + +import { interval$ } from '../../components/controls/select_interval'; +import { severity$ } from '../../components/controls/select_severity'; +import { showCharts$ } from '../../components/controls/checkbox_showcharts'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { + defaultMessage: 'Anomaly Explorer', + }), + href: '', + }, +]; + +export const explorerRoute: MlRoute = { + path: '/explorer', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index } = queryString.parse(location.search); + const { context } = useResolver(index, undefined, config, { + ...basicResolvers(deps), + jobs: mlJobService.loadJobsWrapper, + }); + const { _a, _g } = queryString.parse(location.search); + let appState: any = {}; + let globalState: any = {}; + try { + appState = decode(_a); + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global or app state'); + } + + if (appState.mlExplorerSwimlane === undefined) { + appState.mlExplorerSwimlane = {}; + } + + if (appState.mlExplorerFilter === undefined) { + appState.mlExplorerFilter = {}; + } + + appState.fetch = () => {}; + appState.on = () => {}; + appState.off = () => {}; + appState.save = () => {}; + globalState.fetch = () => {}; + globalState.on = () => {}; + globalState.off = () => {}; + globalState.save = () => {}; + + return ( + + + + ); +}; + +class AppState { + fetch() {} + on() {} + off() {} + save() {} +} + +const ExplorerWrapper: FC<{ globalState: any; appState: any }> = ({ globalState, appState }) => { + const subscriptions = new Subscription(); + + const { jobSelectService$, unsubscribeFromGlobalState } = jobSelectServiceFactory(globalState); + appState = getExplorerDefaultAppState(); + const { mlExplorerFilter, mlExplorerSwimlane } = appState; + window.setTimeout(() => { + // Pass the current URL AppState on to anomaly explorer's reactive state. + // After this hand-off, the appState stored in explorerState$ is the single + // source of truth. + explorerService.setAppState({ mlExplorerSwimlane, mlExplorerFilter }); + + // Now that appState in explorerState$ is the single source of truth, + // subscribe to it and update the actual URL appState on changes. + subscriptions.add( + explorerService.appState$.subscribe((appStateIn: ExplorerAppState) => { + // appState.fetch(); + appState.mlExplorerFilter = appStateIn.mlExplorerFilter; + appState.mlExplorerSwimlane = appStateIn.mlExplorerSwimlane; + // appState.save(); + }) + ); + }); + + subscriptions.add(subscribeAppStateToObservable(AppState, 'mlShowCharts', showCharts$, () => {})); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => {}) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => {}) + ); + + if (globalState.time) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + + useEffect(() => { + return () => { + subscriptions.unsubscribe(); + unsubscribeFromGlobalState(); + }; + }); + + return ( +
+ +
+ ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts new file mode 100644 index 00000000000000..89ed35d5588f2d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/index.ts @@ -0,0 +1,15 @@ +/* + * 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 * from './overview'; +export * from './jobs_list'; +export * from './new_job'; +export * from './datavisualizer'; +export * from './settings'; +export * from './data_frame_analytics'; +export * from './timeseriesexplorer'; +export * from './explorer'; +export * from './access_denied'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx new file mode 100644 index 00000000000000..e61c24426bde99 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { JobsPage } from '../../jobs/jobs_list'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { + defaultMessage: 'Job Management', + }), + href: '', + }, +]; + +export const jobListRoute: MlRoute = { + path: '/jobs', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config, deps }) => { + const { context } = useResolver(undefined, undefined, config, basicResolvers(deps)); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/graph/server/lib/pre/index.js b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts similarity index 62% rename from x-pack/legacy/plugins/graph/server/lib/pre/index.js rename to x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts index 8d2e15f612bf10..b226b75743ca61 100644 --- a/x-pack/legacy/plugins/graph/server/lib/pre/index.js +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index.ts @@ -4,5 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getCallClusterPre } from './get_call_cluster_pre'; -export { verifyApiAccessPre } from './verify_api_access_pre'; +export * from './index_or_search'; +export * from './job_type'; +export * from './new_job'; +export * from './wizard'; +export * from './recognize'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx new file mode 100644 index 00000000000000..b81058a9c89af3 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -0,0 +1,90 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { MlRoute, PageLoader, PageDependencies } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/index_or_search'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; +import { KibanaConfigTypeFix } from '../../../contexts/kibana'; +import { checkBasicLicense } from '../../../license/check_license'; +import { loadIndexPatterns } from '../../../util/index_utils'; +import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check'; + +enum MODE { + NEW_JOB, + DATAVISUALIZER, +} + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { + defaultMessage: 'Create job', + }), + href: '', + }, +]; + +export const indexOrSearchRoute: MlRoute = { + path: '/jobs/new_job/step/index_or_search', + render: (props, config, deps) => ( + + ), + breadcrumbs, +}; + +export const dataVizIndexOrSearchRoute: MlRoute = { + path: '/datavisualizer_index_select', + render: (props, config, deps) => ( + + ), + breadcrumbs, +}; + +const PageWrapper: FC<{ + config: KibanaConfigTypeFix; + nextStepPath: string; + deps: PageDependencies; + mode: MODE; +}> = ({ config, nextStepPath, deps, mode }) => { + const newJobResolvers = { + ...basicResolvers(deps), + preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), + }; + const dataVizResolvers = { + checkBasicLicense, + loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), + checkGetJobsPrivilege, + checkMlNodesAvailable, + }; + + const { context } = useResolver( + undefined, + undefined, + config, + mode === MODE.NEW_JOB ? newJobResolvers : dataVizResolvers + ); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx new file mode 100644 index 00000000000000..e537a186ec784f --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -0,0 +1,43 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +// @ts-ignore +import queryString from 'query-string'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../jobs/new_job/pages/job_type'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { + defaultMessage: 'Create job', + }), + href: '', + }, +]; + +export const jobTypeRoute: MlRoute = { + path: '/jobs/new_job/step/job_type', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context } = useResolver(index, savedSearchId, config, basicResolvers(deps)); + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx new file mode 100644 index 00000000000000..b110434f6f0a8e --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/new_job.tsx @@ -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 React, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { Redirect } from 'react-router-dom'; + +import { MlRoute } from '../../router'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.jobWizardLabel', { + defaultMessage: 'Create job', + }), + href: '#/jobs/new_job', + }, +]; + +export const newJobRoute: MlRoute = { + path: '/jobs/new_job', + render: () => , + breadcrumbs, +}; + +const Page: FC = () => { + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx new file mode 100644 index 00000000000000..4f5085facfb298 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -0,0 +1,66 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import queryString from 'query-string'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { basicResolvers } from '../../resolvers'; +import { Page } from '../../../jobs/new_job/recognize'; +import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; +import { mlJobService } from '../../../services/job_service'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { + defaultMessage: 'Select index or search', + }), + href: '', + }, +]; + +export const recognizeRoute: MlRoute = { + path: '/jobs/new_job/recognize', + render: (props, config, deps) => , + breadcrumbs, +}; + +export const checkViewOrCreateRoute: MlRoute = { + path: '/modules/check_view_or_create', + render: (props, config, deps) => ( + + ), + breadcrumbs: [], +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { id, index, savedSearchId } = queryString.parse(location.search); + const { context, results } = useResolver(index, savedSearchId, config, { + ...basicResolvers(deps), + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }); + + return ( + + + + ); +}; + +const CheckViewOrCreateWrapper: FC = ({ location, config, deps }) => { + const { id: moduleId, index: indexPatternId } = queryString.parse(location.search); + // the single resolver checkViewOrCreateJobs redirects only. so will always reject + useResolver(undefined, undefined, config, { + checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), + }); + return null; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx new file mode 100644 index 00000000000000..ea1baefdce0d1b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -0,0 +1,121 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import queryString from 'query-string'; + +import { basicResolvers } from '../../resolvers'; +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; +import { Page } from '../../../jobs/new_job/pages/new_job'; +import { JOB_TYPE } from '../../../../../common/constants/new_job'; +import { mlJobService } from '../../../services/job_service'; +import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; +import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; + +interface WizardPageProps extends PageProps { + jobType: JOB_TYPE; +} + +const createJobBreadcrumbs = { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.createJobLabel', { + defaultMessage: 'Create job', + }), + href: '#/jobs/new_job', +}; + +const baseBreadcrumbs = [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, createJobBreadcrumbs]; + +const singleMetricBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { + defaultMessage: 'Single metric', + }), + href: '', + }, +]; + +const multiMetricBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { + defaultMessage: 'Multi metric', + }), + href: '', + }, +]; + +const populationBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { + defaultMessage: 'Population', + }), + href: '', + }, +]; + +const advancedBreadcrumbs = [ + ...baseBreadcrumbs, + { + text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { + defaultMessage: 'Advanced configuration', + }), + href: '', + }, +]; + +export const singleMetricRoute: MlRoute = { + path: '/jobs/new_job/single_metric', + render: (props, config, deps) => ( + + ), + breadcrumbs: singleMetricBreadcrumbs, +}; + +export const multiMetricRoute: MlRoute = { + path: '/jobs/new_job/multi_metric', + render: (props, config, deps) => ( + + ), + breadcrumbs: multiMetricBreadcrumbs, +}; + +export const populationRoute: MlRoute = { + path: '/jobs/new_job/population', + render: (props, config, deps) => ( + + ), + breadcrumbs: populationBreadcrumbs, +}; + +export const advancedRoute: MlRoute = { + path: '/jobs/new_job/advanced', + render: (props, config, deps) => ( + + ), + breadcrumbs: advancedBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, jobType, deps }) => { + const { index, savedSearchId } = queryString.parse(location.search); + const { context, results } = useResolver(index, savedSearchId, config, { + ...basicResolvers(deps), + privileges: checkCreateJobsPrivilege, + jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), + existingJobsAndGroups: mlJobService.getJobAndGroupIds, + }); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx new file mode 100644 index 00000000000000..fe9f4336148f3c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/overview.tsx @@ -0,0 +1,60 @@ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { Redirect } from 'react-router-dom'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { OverviewPage } from '../../overview'; + +import { checkFullLicense } from '../../license/check_license'; +import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { getMlNodeCount } from '../../ml_nodes_check'; +import { loadMlServerInfo } from '../../services/ml_server_info'; +import { ML_BREADCRUMB } from '../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + { + text: i18n.translate('xpack.ml.overview.overviewLabel', { + defaultMessage: 'Overview', + }), + href: '#/overview', + }, +]; + +export const overviewRoute: MlRoute = { + path: '/overview', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + loadMlServerInfo, + }); + + return ( + + + + ); +}; + +export const appRootRoute: MlRoute = { + path: '/', + render: () => , + breadcrumbs: [], +}; + +const Page: FC = () => { + return ; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx new file mode 100644 index 00000000000000..56ff57f6610b27 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { CalendarsList } from '../../../settings/calendars'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + href: '#/settings/calendars_list', + }, +]; + +export const calendarListRoute: MlRoute = { + path: '/settings/calendars_list', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canCreateCalendar = checkPermission('canCreateCalendar'); + const canDeleteCalendar = checkPermission('canDeleteCalendar'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx new file mode 100644 index 00000000000000..fb68f103e1b77b --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -0,0 +1,92 @@ +/* + * 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. + */ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { NewCalendar } from '../../../settings/calendars'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +enum MODE { + NEW, + EDIT, +} + +interface NewCalendarPageProps extends PageProps { + mode: MODE; +} + +const newBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/calendars_list/new_calendar', + }, +]; + +const editBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/calendars_list/edit_calendar', + }, +]; + +export const newCalendarRoute: MlRoute = { + path: '/settings/calendars_list/new_calendar', + render: (props, config, deps) => ( + + ), + breadcrumbs: newBreadcrumbs, +}; + +export const editCalendarRoute: MlRoute = { + path: '/settings/calendars_list/edit_calendar/:calendarId', + render: (props, config, deps) => ( + + ), + breadcrumbs: editBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, mode }) => { + let calendarId: string | undefined; + if (mode === MODE.EDIT) { + const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); + calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; + } + + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + const canCreateCalendar = checkPermission('canCreateCalendar'); + const canDeleteCalendar = checkPermission('canDeleteCalendar'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx new file mode 100644 index 00000000000000..cb19883e962c1d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -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. + */ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { FilterLists } from '../../../settings/filter_lists'; + +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +const breadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + href: '#/settings/filter_lists', + }, +]; + +export const filterListRoute: MlRoute = { + path: '/settings/filter_lists', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canCreateFilter = checkPermission('canCreateFilter'); + const canDeleteFilter = checkPermission('canDeleteFilter'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx new file mode 100644 index 00000000000000..7a596a488ddb6c --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -0,0 +1,98 @@ +/* + * 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. + */ +/* + * 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, { FC } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { EditFilterList } from '../../../settings/filter_lists'; +import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; + +enum MODE { + NEW, + EDIT, +} + +interface NewFilterPageProps extends PageProps { + mode: MODE; +} + +const newBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { + defaultMessage: 'Create', + }), + href: '#/settings/filter_lists/new', + }, +]; + +const editBreadcrumbs = [ + ML_BREADCRUMB, + SETTINGS, + { + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { + defaultMessage: 'Edit', + }), + href: '#/settings/filter_lists/edit', + }, +]; + +export const newFilterListRoute: MlRoute = { + path: '/settings/filter_lists/new_filter_list', + render: (props, config, deps) => ( + + ), + breadcrumbs: newBreadcrumbs, +}; + +export const editFilterListRoute: MlRoute = { + path: '/settings/filter_lists/edit_filter_list/:filterId', + render: (props, config, deps) => ( + + ), + breadcrumbs: editBreadcrumbs, +}; + +const PageWrapper: FC = ({ location, config, mode }) => { + let filterId: string | undefined; + if (mode === MODE.EDIT) { + const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); + filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; + } + + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + checkMlNodesAvailable, + }); + + const canCreateFilter = checkPermission('canCreateFilter'); + const canDeleteFilter = checkPermission('canDeleteFilter'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/index.ts new file mode 100644 index 00000000000000..f638b78e05fb1a --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/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. + */ + +export * from './settings'; +export * from './calendar_list'; +export * from './calendar_new_edit'; +export * from './filter_list'; +export * from './filter_list_new_edit'; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx new file mode 100644 index 00000000000000..b62ecc0539e723 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -0,0 +1,46 @@ +/* + * 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. + */ +/* + * 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, { FC } from 'react'; + +import { MlRoute, PageLoader, PageProps } from '../../router'; +import { useResolver } from '../../use_resolver'; + +import { checkFullLicense } from '../../../license/check_license'; +import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; +import { Settings } from '../../../settings'; +import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; + +const breadcrumbs = [ML_BREADCRUMB, SETTINGS]; + +export const settingsRoute: MlRoute = { + path: '/settings', + render: (props, config, deps) => , + breadcrumbs, +}; + +const PageWrapper: FC = ({ config }) => { + const { context } = useResolver(undefined, undefined, config, { + checkFullLicense, + checkGetJobsPrivilege, + getMlNodeCount, + }); + + const canGetFilters = checkPermission('canGetFilters'); + const canGetCalendars = checkPermission('canGetCalendars'); + + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx new file mode 100644 index 00000000000000..a40bbfa214b281 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -0,0 +1,155 @@ +/* + * 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, { FC, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { decode } from 'rison-node'; +import moment from 'moment'; +import { Subscription } from 'rxjs'; + +// @ts-ignore +import queryString from 'query-string'; +import { timefilter } from 'ui/timefilter'; +import { MlRoute, PageLoader, PageProps } from '../router'; +import { useResolver } from '../use_resolver'; +import { basicResolvers } from '../resolvers'; +import { TimeSeriesExplorer } from '../../timeseriesexplorer'; +import { mlJobService } from '../../services/job_service'; +import { APP_STATE_ACTION } from '../../timeseriesexplorer/timeseriesexplorer_constants'; +import { subscribeAppStateToObservable } from '../../util/app_state_utils'; +import { interval$ } from '../../components/controls/select_interval'; +import { severity$ } from '../../components/controls/select_severity'; +import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../breadcrumbs'; + +export const timeSeriesExplorerRoute: MlRoute = { + path: '/timeseriesexplorer', + render: (props, config, deps) => , + breadcrumbs: [ + ML_BREADCRUMB, + ANOMALY_DETECTION_BREADCRUMB, + { + text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { + defaultMessage: 'Single Metric Viewer', + }), + href: '', + }, + ], +}; + +const PageWrapper: FC = ({ location, config, deps }) => { + const { context } = useResolver('', undefined, config, { + ...basicResolvers(deps), + jobs: mlJobService.loadJobsWrapper, + }); + const { _a, _g } = queryString.parse(location.search); + let appState: any = {}; + let globalState: any = {}; + try { + appState = decode(_a); + globalState = decode(_g); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Could not parse global or app state'); + } + if (appState.mlTimeSeriesExplorer === undefined) { + appState.mlTimeSeriesExplorer = {}; + } + globalState.fetch = () => {}; + globalState.on = () => {}; + globalState.off = () => {}; + globalState.save = () => {}; + + return ( + + + + ); +}; + +class AppState { + fetch() {} + on() {} + off() {} + save() {} +} + +const TimeSeriesExplorerWrapper: FC<{ globalState: any; appState: any; config: any }> = ({ + globalState, + appState, + config, +}) => { + if (globalState.time) { + timefilter.setTime({ + from: globalState.time.from, + to: globalState.time.to, + }); + } + + const subscriptions = new Subscription(); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$, () => {}) + ); + subscriptions.add( + subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$, () => {}) + ); + + const appStateHandler = (action: string, payload: any) => { + switch (action) { + case APP_STATE_ACTION.CLEAR: + delete appState.mlTimeSeriesExplorer.detectorIndex; + delete appState.mlTimeSeriesExplorer.entities; + delete appState.mlTimeSeriesExplorer.forecastId; + break; + + case APP_STATE_ACTION.GET_DETECTOR_INDEX: + return appState.mlTimeSeriesExplorer.detectorIndex; + case APP_STATE_ACTION.SET_DETECTOR_INDEX: + appState.mlTimeSeriesExplorer.detectorIndex = payload; + break; + + case APP_STATE_ACTION.GET_ENTITIES: + return appState.mlTimeSeriesExplorer.entities; + case APP_STATE_ACTION.SET_ENTITIES: + appState.mlTimeSeriesExplorer.entities = payload; + break; + + case APP_STATE_ACTION.GET_FORECAST_ID: + return appState.mlTimeSeriesExplorer.forecastId; + case APP_STATE_ACTION.SET_FORECAST_ID: + appState.mlTimeSeriesExplorer.forecastId = payload; + break; + + case APP_STATE_ACTION.GET_ZOOM: + return appState.mlTimeSeriesExplorer.zoom; + case APP_STATE_ACTION.SET_ZOOM: + appState.mlTimeSeriesExplorer.zoom = payload; + break; + case APP_STATE_ACTION.UNSET_ZOOM: + delete appState.mlTimeSeriesExplorer.zoom; + break; + } + }; + + useEffect(() => { + return () => { + subscriptions.unsubscribe(); + }; + }); + + const tzConfig = config.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + + return ( + + ); +}; diff --git a/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts new file mode 100644 index 00000000000000..f74260c06567e0 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/routing/use_resolver.ts @@ -0,0 +1,70 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { + getIndexPatternById, + getIndexPatternsContract, + getIndexPatternAndSavedSearch, +} from '../util/index_utils'; +import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; +import { ResolverResults, Resolvers } from './resolvers'; +import { KibanaConfigTypeFix, KibanaContextValue } from '../contexts/kibana'; + +export const useResolver = ( + indexPatternId: string | undefined, + savedSearchId: string | undefined, + config: KibanaConfigTypeFix, + resolvers: Resolvers +): { context: KibanaContextValue; results: ResolverResults } => { + const funcNames = Object.keys(resolvers); // Object.entries gets this wrong?! + const funcs = Object.values(resolvers); // Object.entries gets this wrong?! + const tempResults = funcNames.reduce((p, c) => { + p[c] = {}; + return p; + }, {} as ResolverResults); + + const [context, setContext] = useState(null); + const [results, setResults] = useState(tempResults); + + useEffect(() => { + (async () => { + try { + const res = await Promise.all(funcs.map(r => r())); + res.forEach((r, i) => (tempResults[funcNames[i]] = r)); + setResults(tempResults); + + if (indexPatternId !== undefined || savedSearchId !== undefined) { + // note, currently we're using our own kibana context that requires a current index pattern to be set + // this means, if the page uses this context, useResolver must be passed a string for the index pattern id + // and loadIndexPatterns must be part of the resolvers. + const { indexPattern, savedSearch } = + savedSearchId !== undefined + ? await getIndexPatternAndSavedSearch(savedSearchId) + : { savedSearch: null, indexPattern: await getIndexPatternById(indexPatternId!) }; + + const { combinedQuery } = createSearchItems(config, indexPattern!, savedSearch); + + setContext({ + combinedQuery, + currentIndexPattern: indexPattern, + currentSavedSearch: savedSearch, + indexPatterns: getIndexPatternsContract()!, + kibanaConfig: config, + }); + } else { + setContext({}); + } + } catch (error) { + // quietly fail. Let the resolvers handle the redirection if any fail to resolve + // eslint-disable-next-line no-console + console.error('ML page loading resolver', error); + } + })(); + }, []); + + return { context, results }; +}; diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts index a3096a942a7c71..b9ed83eeffba16 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.d.ts @@ -35,10 +35,10 @@ declare interface JobService { end: number | undefined ): Promise; createResultsUrl(jobId: string[], start: number, end: number, location: string): string; - getJobAndGroupIds(): ExistingJobsAndGroups; + getJobAndGroupIds(): Promise; searchPreview(job: CombinedJob): Promise>; getJob(jobId: string): CombinedJob; - loadJobsWrapper(): Promise; + loadJobsWrapper(): Promise; } export const mlJobService: JobService; diff --git a/x-pack/legacy/plugins/ml/public/application/services/job_service.js b/x-pack/legacy/plugins/ml/public/application/services/job_service.js index 90aa5d8d66faac..dbe81df0f04719 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/job_service.js +++ b/x-pack/legacy/plugins/ml/public/application/services/job_service.js @@ -912,7 +912,7 @@ function createResultsUrl(jobIds, start, end, resultsPage) { let path = ''; if (resultsPage !== undefined) { - path += 'ml#/'; + path += '#/'; path += resultsPage; } diff --git a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts index f79515c80556a3..9d5c33d6cfc5c0 100644 --- a/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts +++ b/x-pack/legacy/plugins/ml/public/application/services/new_job_capabilities_service.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedSearchLoader } from 'src/legacy/core_plugins/kibana/public/discover/types'; - import { Field, Aggregation, @@ -20,18 +18,16 @@ import { IndexPatternsContract, } from '../../../../../../../src/plugins/data/public'; import { ml } from './ml_api_service'; +import { getIndexPatternAndSavedSearch } from '../util/index_utils'; // called in the angular routing resolve block to initialize the // newJobCapsService with the currently selected index pattern export function loadNewJobCapabilities( - indexPatterns: IndexPatternsContract, - savedSearches: SavedSearchLoader, - $route: Record + indexPatternId: string, + savedSearchId: string, + indexPatterns: IndexPatternsContract ) { return new Promise(async (resolve, reject) => { - // get the index pattern id or saved search id from the url params - const { index: indexPatternId, savedSearchId } = $route.current.params; - if (indexPatternId !== undefined) { // index pattern is being used const indexPattern: IndexPattern = await indexPatterns.get(indexPatternId); @@ -40,8 +36,13 @@ export function loadNewJobCapabilities( } else if (savedSearchId !== undefined) { // saved search is being used // load the index pattern from the saved search - const savedSearch = await savedSearches.get(savedSearchId); - const indexPattern = savedSearch.searchSource.getField('index')!; + const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); + if (indexPattern === null) { + // eslint-disable-next-line no-console + console.error('Cannot retrieve index pattern from saved search'); + reject(); + return; + } await newJobCapsService.initializeFromIndexPattern(indexPattern); resolve(newJobCapsService.newJobCaps); } else { diff --git a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts b/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts deleted file mode 100644 index bd04003c9eca4e..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/breadcrumbs.ts +++ /dev/null @@ -1,86 +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 { i18n } from '@kbn/i18n'; -import { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; - -export function getSettingsBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB, SETTINGS]; -} - -export function getCalendarManagementBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - href: '#/settings/calendars_list', - }, - ]; -} - -export function getCreateCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/calendars_list/new_calendar', - }, - ]; -} - -export function getEditCalendarBreadcrumbs() { - return [ - ...getCalendarManagementBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/calendars_list/edit_calendar', - }, - ]; -} - -export function getFilterListsBreadcrumbs() { - return [ - ...getSettingsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - href: '#/settings/filter_lists', - }, - ]; -} - -export function getCreateFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { - defaultMessage: 'Create', - }), - href: '#/settings/filter_lists/new', - }, - ]; -} - -export function getEditFilterListBreadcrumbs() { - return [ - ...getFilterListsBreadcrumbs(), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { - defaultMessage: 'Edit', - }), - href: '#/settings/filter_lists/edit', - }, - ]; -} diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index 408042fb70cbac..267fb3930121bb 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -181,7 +181,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` grow={false} > - -`; - -uiRoutes - .when('/settings/calendars_list/new_calendar', { - template, - k7Breadcrumbs: getCreateCalendarBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - checkMlNodesAvailable, - }, - }) - .when('/settings/calendars_list/edit_calendar/:calendarId', { - template, - k7Breadcrumbs: getEditCalendarBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - checkMlNodesAvailable, - }, - }); - -module.directive('mlNewCalendar', function($route: any) { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - calendarId: $route.current.params.calendarId, - canCreateCalendar: checkPermission('canCreateCalendar'), - canDeleteCalendar: checkPermission('canDeleteCalendar'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts index aa8b2ec2c29c97..5e008e4796d1cb 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { NewCalendar } from './new_calendar'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts index d6de538d6388a8..002a88ec03f0d1 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.d.ts @@ -7,7 +7,7 @@ import { FC } from 'react'; declare const NewCalendar: FC<{ - calendarId: string; + calendarId?: string; canCreateCalendar: boolean; canDeleteCalendar: boolean; }>; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index feabd60d8d3a0a..c9fe2503b0c5b4 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -18,7 +18,6 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import chrome from 'ui/chrome'; import { toastNotifications } from 'ui/notify'; import { NavigationMenu } from '../../../components/navigation_menu'; @@ -153,7 +152,7 @@ export const NewCalendar = injectI18n(class NewCalendar extends Component { try { await ml.addCalendar(calendar); - window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`; + window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); @@ -176,7 +175,7 @@ export const NewCalendar = injectI18n(class NewCalendar extends Component { try { await ml.updateCalendar(calendar); - window.location = `${chrome.getBasePath()}/app/ml#/settings/calendars_list`; + window.location = '#/settings/calendars_list'; } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts new file mode 100644 index 00000000000000..88aa20ea063208 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { NewCalendar } from './edit'; +export { CalendarsList } from './list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx deleted file mode 100644 index 1b90a27c07ada0..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/directive.tsx +++ /dev/null @@ -1,58 +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 'ngreact'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { getCalendarManagementBreadcrumbs } from '../../breadcrumbs'; - -import { CalendarsList } from './calendars_list'; - -const template = ` -
- -`; - -uiRoutes.when('/settings/calendars_list', { - template, - k7Breadcrumbs: getCalendarManagementBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -module.directive('mlCalendarsList', function() { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - canCreateCalendar: checkPermission('canCreateCalendar'), - canDeleteCalendar: checkPermission('canDeleteCalendar'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts index aa8b2ec2c29c97..dcf8ade3301b39 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { CalendarsList } from './calendars_list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index 1932ff3d83efa7..7958546f3a0cff 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -70,7 +70,7 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` "toolsRight": Array [ diff --git a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js index 3a5c8eec31c067..285f9423cd4c10 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -15,8 +15,6 @@ import { EuiInMemoryTable, } from '@elastic/eui'; -import chrome from 'ui/chrome'; - import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; @@ -55,7 +53,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ truncateText: true, render: (id) => ( {id} @@ -98,7 +96,7 @@ export const CalendarsListTable = injectI18n(function CalendarsListTable({ size="s" data-test-subj="mlCalendarButtonCreate" key="new_calendar_button" - href={`${chrome.getBasePath()}/app/ml#/settings/calendars_list/new_calendar`} + href="#/settings/calendars_list/new_calendar" isDisabled={(canCreateCalendar === false || mlNodesAvailable === false)} > - -`; - -uiRoutes - .when('/settings/filter_lists/new_filter_list', { - template, - k7Breadcrumbs: getCreateFilterListBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, - }) - .when('/settings/filter_lists/edit_filter_list/:filterId', { - template, - k7Breadcrumbs: getEditFilterListBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, - }); - -module.directive('mlEditFilterList', function($route: any) { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - filterId: $route.current.params.filterId, - canCreateFilter: checkPermission('canCreateFilter'), - canDeleteFilter: checkPermission('canDeleteFilter'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts index 71d82d7694cf05..56ef8745cb28bd 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.d.ts @@ -7,7 +7,7 @@ import { FC } from 'react'; declare const EditFilterList: FC<{ - filterId: string; + filterId?: string; canCreateFilter: boolean; canDeleteFilter: boolean; }>; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts index aa8b2ec2c29c97..52b35f361777f4 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/edit/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { EditFilterList } from './edit_filter_list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts index 6a942d5c251df9..52dcda9b3f7c0f 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/index.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -import './edit'; -import './list'; +export { EditFilterList } from './edit'; +export { FilterLists } from './list'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx deleted file mode 100644 index 7b572344c603b5..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/directive.tsx +++ /dev/null @@ -1,58 +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 'ngreact'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -// @ts-ignore -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml', ['react']); - -import uiRoutes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { checkFullLicense } from '../../../license/check_license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; -import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; -import { getFilterListsBreadcrumbs } from '../../breadcrumbs'; - -import { FilterLists } from './filter_lists'; - -const template = ` -
- -`; - -uiRoutes.when('/settings/filter_lists', { - template, - k7Breadcrumbs: getFilterListsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -module.directive('mlFilterLists', function() { - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - const props = { - canCreateFilter: checkPermission('canCreateFilter'), - canDeleteFilter: checkPermission('canDeleteFilter'), - }; - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts index aa8b2ec2c29c97..2e5cc371e317d7 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './directive'; +export { FilterLists } from './filter_lists'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js index a5cc1ed761b567..85bba8e4fe7443 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/legacy/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -26,7 +26,6 @@ import { import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; import { DeleteFilterListModal } from '../components/delete_filter_list_modal'; @@ -68,7 +67,7 @@ function NewFilterButton({ canCreateFilter }) { return ( @@ -89,7 +88,7 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - + {id} ), diff --git a/x-pack/legacy/plugins/ml/public/application/settings/index.ts b/x-pack/legacy/plugins/ml/public/application/settings/index.ts index d9fc996ae4a301..db74dcb1a1846e 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/index.ts +++ b/x-pack/legacy/plugins/ml/public/application/settings/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './settings_directive'; -import './calendars'; -import './filter_lists'; +export { Settings } from './settings'; diff --git a/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx index 3c1ae2973721a9..225f39fc6f419a 100644 --- a/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx +++ b/x-pack/legacy/plugins/ml/public/application/settings/settings.tsx @@ -19,7 +19,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; -import { useUiChromeContext } from '../contexts/ui/use_ui_chrome_context'; import { NavigationMenu } from '../components/navigation_menu'; interface Props { @@ -28,8 +27,6 @@ interface Props { } export const Settings: FC = ({ canGetFilters, canGetCalendars }) => { - const basePath = useUiChromeContext().getBasePath(); - return ( @@ -53,7 +50,7 @@ export const Settings: FC = ({ canGetFilters, canGetCalendars }) => { data-test-subj="ml_calendar_mng_button" size="l" color="primary" - href={`${basePath}/app/ml#/settings/calendars_list`} + href="#/settings/calendars_list" isDisabled={canGetCalendars === false} > = ({ canGetFilters, canGetCalendars }) => { data-test-subj="ml_filter_lists_button" size="l" color="primary" - href={`${basePath}/app/ml#/settings/filter_lists`} + href="#/settings/filter_lists" isDisabled={canGetFilters === false} > - -`; - -uiRoutes.when('/settings', { - template, - k7Breadcrumbs: getSettingsBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - mlNodeCount: getMlNodeCount, - }, -}); - -import { Settings } from './settings'; - -module.directive('mlSettings', function() { - const canGetFilters = checkPermission('canGetFilters'); - const canGetCalendars = checkPermission('canGetCalendars'); - - return { - restrict: 'E', - replace: false, - scope: {}, - link(scope: ng.IScope, element: ng.IAugmentedJQuery) { - timefilter.disableTimeRangeSelector(); - timefilter.disableAutoRefreshSelector(); - - ReactDOM.render( - - - , - element[0] - ); - }, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js deleted file mode 100644 index 2aa4c845b125d7..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/breadcrumbs.js +++ /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 { ML_BREADCRUMB, ANOMALY_DETECTION_BREADCRUMB } from '../../breadcrumbs'; -import { i18n } from '@kbn/i18n'; - - -export function getSingleMetricViewerBreadcrumbs() { - // Whilst top level nav menu with tabs remains, - // use root ML breadcrumb. - return [ - ML_BREADCRUMB, - ANOMALY_DETECTION_BREADCRUMB, - { - text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { - defaultMessage: 'Single Metric Viewer' - }), - href: '' - } - - ]; -} - diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js deleted file mode 100644 index 5aa6cfe8835ad5..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.js +++ /dev/null @@ -1,11 +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 './timeseriesexplorer_directive'; -import './timeseriesexplorer_route'; -import './timeseries_search_service'; -import '../components/job_selector'; -import '../components/chart_tooltip'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/index.ts new file mode 100644 index 00000000000000..6877e8ef754fdb --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/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 { TimeSeriesExplorer } from './timeseriesexplorer'; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts new file mode 100644 index 00000000000000..ac4bc6186e5b47 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.d.ts @@ -0,0 +1,15 @@ +/* + * 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 { Timefilter } from 'ui/timefilter'; +import { FC } from 'react'; + +declare const TimeSeriesExplorer: FC<{ + appStateHandler: (action: string, payload: any) => void; + dateFormatTz: string; + globalState: any; + timefilter: Timefilter; +}>; diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index a70e1d38784e98..99eb4beb977da1 100644 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -142,7 +142,7 @@ const TimeSeriesExplorerPage = ({ children, jobSelectorProps, loading, resizeRef If we'd just show no progress bar when not loading it would result in a flickering height effect. */} {!loading && ()} -
+
{children}
diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js deleted file mode 100644 index 048a8dbc4a6db8..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_directive.js +++ /dev/null @@ -1,111 +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 { get } from 'lodash'; -import moment from 'moment-timezone'; -import { Subscription } from 'rxjs'; - -import React from 'react'; -import ReactDOM from 'react-dom'; - -import { uiModules } from 'ui/modules'; -const module = uiModules.get('apps/ml'); - -import { timefilter } from 'ui/timefilter'; -import { I18nContext } from 'ui/i18n'; - -import '../components/controls'; - -import { severity$ } from '../components/controls/select_severity/select_severity'; -import { interval$ } from '../components/controls/select_interval/select_interval'; -import { subscribeAppStateToObservable } from '../util/app_state_utils'; - -import { TimeSeriesExplorer } from './timeseriesexplorer'; -import { APP_STATE_ACTION } from './timeseriesexplorer_constants'; - -module.directive('mlTimeSeriesExplorer', function ($injector) { - function link($scope, $element) { - const globalState = $injector.get('globalState'); - const AppState = $injector.get('AppState'); - const config = $injector.get('config'); - - const subscriptions = new Subscription(); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectInterval', interval$)); - subscriptions.add(subscribeAppStateToObservable(AppState, 'mlSelectSeverity', severity$)); - - $scope.appState = new AppState({ mlTimeSeriesExplorer: {} }); - - const appStateHandler = (action, payload) => { - $scope.appState.fetch(); - switch (action) { - case APP_STATE_ACTION.CLEAR: - delete $scope.appState.mlTimeSeriesExplorer.detectorIndex; - delete $scope.appState.mlTimeSeriesExplorer.entities; - delete $scope.appState.mlTimeSeriesExplorer.forecastId; - break; - - case APP_STATE_ACTION.GET_DETECTOR_INDEX: - return get($scope, 'appState.mlTimeSeriesExplorer.detectorIndex'); - case APP_STATE_ACTION.SET_DETECTOR_INDEX: - $scope.appState.mlTimeSeriesExplorer.detectorIndex = payload; - break; - - case APP_STATE_ACTION.GET_ENTITIES: - return get($scope, 'appState.mlTimeSeriesExplorer.entities', {}); - case APP_STATE_ACTION.SET_ENTITIES: - $scope.appState.mlTimeSeriesExplorer.entities = payload; - break; - - case APP_STATE_ACTION.GET_FORECAST_ID: - return get($scope, 'appState.mlTimeSeriesExplorer.forecastId'); - case APP_STATE_ACTION.SET_FORECAST_ID: - $scope.appState.mlTimeSeriesExplorer.forecastId = payload; - break; - - case APP_STATE_ACTION.GET_ZOOM: - return get($scope, 'appState.mlTimeSeriesExplorer.zoom'); - case APP_STATE_ACTION.SET_ZOOM: - $scope.appState.mlTimeSeriesExplorer.zoom = payload; - break; - case APP_STATE_ACTION.UNSET_ZOOM: - delete $scope.appState.mlTimeSeriesExplorer.zoom; - break; - } - $scope.appState.save(); - $scope.$applyAsync(); - }; - - function updateComponent() { - // Pass the timezone to the server for use when aggregating anomalies (by day / hour) for the table. - const tzConfig = config.get('dateFormat:tz'); - const dateFormatTz = (tzConfig !== 'Browser') ? tzConfig : moment.tz.guess(); - - ReactDOM.render( - - - , - $element[0] - ); - } - - $element.on('$destroy', () => { - ReactDOM.unmountComponentAtNode($element[0]); - subscriptions.unsubscribe(); - }); - - updateComponent(); - } - - return { - link, - }; -}); diff --git a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js b/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js deleted file mode 100644 index 63b9b819be315a..00000000000000 --- a/x-pack/legacy/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_route.js +++ /dev/null @@ -1,30 +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 uiRoutes from 'ui/routes'; - -import '../components/controls'; - -import { checkFullLicense } from '../license/check_license'; -import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; -import { mlJobService } from '../services/job_service'; -import { loadIndexPatterns } from '../util/index_utils'; - -import { getSingleMetricViewerBreadcrumbs } from './breadcrumbs'; - -uiRoutes - .when('/timeseriesexplorer/?', { - template: '', - k7Breadcrumbs: getSingleMetricViewerBreadcrumbs, - resolve: { - CheckLicense: checkFullLicense, - privileges: checkGetJobsPrivilege, - indexPatterns: loadIndexPatterns, - mlNodeCount: getMlNodeCount, - jobs: mlJobService.loadJobsWrapper - } - }); diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js index 8aa933eb5e53fe..110795c2d0290c 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.js @@ -12,7 +12,6 @@ import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impa import moment from 'moment'; import rison from 'rison-node'; -import chrome from 'ui/chrome'; import { timefilter } from 'ui/timefilter'; import { CHART_TYPE } from '../explorer/explorer_constants'; @@ -229,7 +228,7 @@ export function getExploreSeriesLink(series) { } }); - return `${chrome.getBasePath()}/app/ml#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; + return `#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; } export function showMultiBucketAnomalyMarker(point) { diff --git a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js index a229113826a2e9..5ef6b592ae2715 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/legacy/plugins/ml/public/application/util/chart_utils.test.js @@ -58,7 +58,7 @@ timefilter.setTime({ describe('getExploreSeriesLink', () => { test('get timeseriesexplorer link', () => { const link = getExploreSeriesLink(seriesConfig); - const expectedLink = `/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + + const expectedLink = `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + diff --git a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts index 99882b0243be8f..2b8838c04cf696 100644 --- a/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/legacy/plugins/ml/public/application/util/index_utils.ts @@ -6,19 +6,17 @@ import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { SavedObjectAttributes, SimpleSavedObject } from 'kibana/public'; import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; -import { SavedSearchLoader } from '../../../../../../../src/legacy/core_plugins/kibana/public/discover/types'; +import { Query } from 'src/plugins/data/public'; import { IndexPattern, IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; - -type IndexPatternSavedObject = SimpleSavedObject; +import { IndexPatternSavedObject, SavedSearchSavedObject } from '../../../common/types/kibana'; let indexPatternCache: IndexPatternSavedObject[] = []; -let fullIndexPatterns: IndexPatternsContract | null = null; +let savedSearchesCache: SavedSearchSavedObject[] = []; +let indexPatternsContract: IndexPatternsContract | null = null; -export function loadIndexPatterns() { - fullIndexPatterns = npStart.plugins.data.indexPatterns; +export function loadIndexPatterns(indexPatterns: IndexPatternsContract) { + indexPatternsContract = indexPatterns; const savedObjectsClient = chrome.getSavedObjectsClient(); return savedObjectsClient .find({ @@ -32,10 +30,33 @@ export function loadIndexPatterns() { }); } +export function loadSavedSearches() { + const savedObjectsClient = chrome.getSavedObjectsClient(); + return savedObjectsClient + .find({ + type: 'search', + perPage: 10000, + }) + .then(response => { + savedSearchesCache = response.savedObjects; + return savedSearchesCache; + }); +} + +export async function loadSavedSearchById(id: string) { + const savedObjectsClient = chrome.getSavedObjectsClient(); + const ss = await savedObjectsClient.get('search', id); + return ss.error === undefined ? ss : null; +} + export function getIndexPatterns() { return indexPatternCache; } +export function getIndexPatternsContract() { + return indexPatternsContract; +} + export function getIndexPatternNames() { return indexPatternCache.map(i => i.attributes && i.attributes.title); } @@ -49,27 +70,44 @@ export function getIndexPatternIdFromName(name: string) { return null; } -export function loadCurrentIndexPattern( - indexPatterns: IndexPatternsContract, - $route: Record -) { - fullIndexPatterns = indexPatterns; - return fullIndexPatterns.get($route.current.params.index); +export async function getIndexPatternAndSavedSearch(savedSearchId: string) { + const resp: { savedSearch: SavedSearchSavedObject | null; indexPattern: IndexPattern | null } = { + savedSearch: null, + indexPattern: null, + }; + + if (savedSearchId === undefined) { + return resp; + } + + const ss = await loadSavedSearchById(savedSearchId); + if (ss === null) { + return resp; + } + const indexPatternId = ss.references.find(r => r.type === 'index-pattern')?.id; + resp.indexPattern = await getIndexPatternById(indexPatternId!); + resp.savedSearch = ss; + return resp; +} + +export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { + const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; + return JSON.parse(search.searchSourceJSON) as { + query: Query; + filter: any[]; + }; } export function getIndexPatternById(id: string): Promise { - if (fullIndexPatterns !== null) { - return fullIndexPatterns.get(id); + if (indexPatternsContract !== null) { + return indexPatternsContract.get(id); } else { throw new Error('Index patterns are not initialized!'); } } -export function loadCurrentSavedSearch( - savedSearches: SavedSearchLoader, - $route: Record -) { - return savedSearches.get($route.current.params.savedSearchId); +export function getSavedSearchById(id: string): SavedSearchSavedObject | undefined { + return savedSearchesCache.find(s => s.id === id); } /** diff --git a/x-pack/legacy/plugins/ml/public/index.scss b/x-pack/legacy/plugins/ml/public/index.scss deleted file mode 100644 index ac3f3fef97c702..00000000000000 --- a/x-pack/legacy/plugins/ml/public/index.scss +++ /dev/null @@ -1,45 +0,0 @@ -// Should import both the EUI constants and any Kibana ones that are considered global -@import 'src/legacy/ui/public/styles/styling_constants'; - -// ML has it's own variables for coloring -@import 'application/variables'; - -// Kibana management page ML section -#kibanaManagementMLSection { - @import 'application/management/index'; -} - -// Protect the rest of Kibana from ML generic namespacing -// SASSTODO: Prefix ml selectors instead -#ml-app { - // App level - @import 'application/app'; - - // Sub applications - @import 'application/data_frame_analytics/index'; - @import 'application/datavisualizer/index'; - @import 'application/explorer/index'; // SASSTODO: This file needs to be rewritten - @import 'application/jobs/index'; // SASSTODO: This collection of sass files has multiple problems - @import 'application/overview/index'; - @import 'application/settings/index'; - @import 'application/timeseriesexplorer/index'; - - // Components - @import 'application/components/annotations/annotation_description_list/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/anomalies_table/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/chart_tooltip/index'; - @import 'application/components/controls/index'; - @import 'application/components/entity_cell/index'; - @import 'application/components/field_title_bar/index'; - @import 'application/components/field_type_icon/index'; - @import 'application/components/influencers_list/index'; - @import 'application/components/items_grid/index'; - @import 'application/components/job_selector/index'; - @import 'application/components/loading_indicator/index'; // SASSTODO: This component should be replaced with EuiLoadingSpinner - @import 'application/components/navigation_menu/index'; - @import 'application/components/rule_editor/index'; // SASSTODO: This file overwrites EUI directly - @import 'application/components/stats_bar/index'; - - // Hacks are last so they can overwrite anything above if needed - @import 'application/hacks'; -} diff --git a/x-pack/legacy/plugins/ml/public/index.ts b/x-pack/legacy/plugins/ml/public/index.ts new file mode 100755 index 00000000000000..0057983104cc09 --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/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 { PluginInitializer } from '../../../../../src/core/public'; +import { MlPlugin, MlPluginSetup, MlPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => new MlPlugin(); + +export { MlPluginSetup, MlPluginStart }; diff --git a/x-pack/legacy/plugins/ml/public/legacy.ts b/x-pack/legacy/plugins/ml/public/legacy.ts new file mode 100644 index 00000000000000..3e007a18f4c5ac --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/legacy.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 { npSetup, npStart } from 'ui/new_platform'; + +import { PluginInitializerContext } from '../../../../../src/core/public'; +import { plugin } from '.'; + +const pluginInstance = plugin({} as PluginInitializerContext); + +export const setup = pluginInstance.setup(npSetup.core, { + npData: npStart.plugins.data, +}); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/x-pack/legacy/plugins/ml/public/plugin.ts b/x-pack/legacy/plugins/ml/public/plugin.ts new file mode 100644 index 00000000000000..f68d1ffe88216d --- /dev/null +++ b/x-pack/legacy/plugins/ml/public/plugin.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin as DataPlugin } from 'src/plugins/data/public'; +import { Plugin, CoreStart, CoreSetup } from '../../../../../src/core/public'; + +export interface MlSetupDependencies { + npData: ReturnType; +} + +export class MlPlugin implements Plugin { + setup(core: CoreSetup, { npData }: MlSetupDependencies) { + core.application.register({ + id: 'ml', + title: 'Machine learning', + async mount(context, params) { + const [coreStart, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./application/app'); + return renderApp(coreStart, depsStart, { + ...params, + indexPatterns: npData.indexPatterns, + npData, + }); + }, + }); + + return {}; + } + + start(core: CoreStart, deps: {}) { + return {}; + } + public stop() {} +} + +export type MlPluginSetup = ReturnType; +export type MlPluginStart = ReturnType; diff --git a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts index e7c896bb36ed84..6b426169799a7b 100644 --- a/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ b/x-pack/legacy/plugins/ml/server/lib/check_privileges/check_privileges.ts @@ -5,7 +5,7 @@ */ import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { XPackMainPlugin } from '../../../../../../legacy/plugins/xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../../xpack_main/server/xpack_main'; import { callWithRequestType } from '../../../common/types/kibana'; import { isSecurityDisabled } from '../../lib/security_utils'; import { upgradeCheckProvider } from './upgrade'; diff --git a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts b/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts index 84d9961b0c6f0f..26fdff73b34606 100644 --- a/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts +++ b/x-pack/legacy/plugins/ml/server/lib/security_utils.d.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { XPackMainPlugin } from '../../../../../legacy/plugins/xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; export function isSecurityDisabled(xpackMainPlugin: XPackMainPlugin): boolean; diff --git a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts index eb2f50b8e92509..5bb0f399821466 100644 --- a/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/legacy/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -128,6 +128,14 @@ function getSearchJsonFromConfig( }, }; + if (query.bool === undefined) { + query.bool = { + must: [], + }; + } else if (query.bool.must === undefined) { + query.bool.must = []; + } + query.bool.must.push({ range: { [timeField]: { diff --git a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts index 727d05605614fb..c468c87d7abc8c 100644 --- a/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts +++ b/x-pack/legacy/plugins/ml/server/new_platform/plugin.ts @@ -12,7 +12,7 @@ import { Logger, PluginInitializerContext, CoreSetup } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CloudSetup } from '../../../../../plugins/cloud/server'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; import { addLinksToSampleDatasets } from '../lib/sample_data_sets'; import { checkLicense } from '../lib/check_license'; // @ts-ignore: could not find declaration file for module diff --git a/x-pack/legacy/plugins/observability/README.md b/x-pack/legacy/plugins/observability/README.md deleted file mode 100644 index f7d8365fe6c802..00000000000000 --- a/x-pack/legacy/plugins/observability/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Observability Shared Resources - -This "faux" plugin serves as a place to statically share resources, helpers, and components across observability plugins. There is some discussion still happening about the best way to do this, but this is one suggested method that will work for now and has the benefit of adopting our pre-defined build and compile tooling out of the box. - -Files found here can be imported from any other x-pack plugin, with the caveat that these shared components should all be exposed from either `public/index` or `server/index` so that the platform can attempt to monitor breaking changes in this shared API. - -# for a file found at `x-pack/legacy/plugins/infra/public/components/Example.tsx` - -```ts -import { ExampleSharedComponent } from '../../../observability/public'; -``` - -### Plugin registration and config - -There is no plugin registration code or config in this folder because it's a "faux" plugin only being used to share code between other plugins. Plugins using this code do not need to register a dependency on this plugin unless this plugin ever exports functionality that relies on Kibana core itself (rather than being static DI components and utilities only, as it is now). - -### Directory structure - -Code meant to be shared by the UI should live in `public/` and be explicity exported from `public/index` while server helpers etc should live in `server/` and be explicitly exported from `server/index`. Code that needs to be shared across client and server should be exported from both places (not put in `common`, etc). diff --git a/x-pack/legacy/plugins/observability/public/components/example_shared_component.tsx b/x-pack/legacy/plugins/observability/public/components/example_shared_component.tsx deleted file mode 100644 index e7cac9e3d70151..00000000000000 --- a/x-pack/legacy/plugins/observability/public/components/example_shared_component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -interface Props { - message?: string; -} - -export function ExampleSharedComponent({ message = 'See how it loads.' }: Props) { - return

This is an example of an observability shared component. {message}

; -} diff --git a/x-pack/legacy/plugins/observability/public/context/kibana_core.tsx b/x-pack/legacy/plugins/observability/public/context/kibana_core.tsx deleted file mode 100644 index ab936ed689edfc..00000000000000 --- a/x-pack/legacy/plugins/observability/public/context/kibana_core.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { createContext, useContext } from 'react'; -import { LegacyCoreStart } from '../../../../../../src/core/public'; - -interface AppMountContext { - core: LegacyCoreStart; -} - -// TODO: Replace CoreStart/CoreSetup with AppMountContext -// see: https://github.com/elastic/kibana/pull/41007 - -export const KibanaCoreContext = createContext({} as AppMountContext['core']); - -export const KibanaCoreContextProvider: React.FC<{ core: AppMountContext['core'] }> = props => ( - -); - -export function useKibanaCore() { - return useContext(KibanaCoreContext); -} diff --git a/x-pack/legacy/plugins/observability/public/index.tsx b/x-pack/legacy/plugins/observability/public/index.tsx deleted file mode 100644 index 49e5a6d787a553..00000000000000 --- a/x-pack/legacy/plugins/observability/public/index.tsx +++ /dev/null @@ -1,9 +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 { KibanaCoreContext, KibanaCoreContextProvider, useKibanaCore } from './context/kibana_core'; -import { ExampleSharedComponent } from './components/example_shared_component'; - -export { ExampleSharedComponent, KibanaCoreContext, KibanaCoreContextProvider, useKibanaCore }; diff --git a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js index c3bf8668af8dc5..329c1558c27106 100644 --- a/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/legacy/plugins/remote_clusters/__jest__/client_integration/helpers/setup_environment.js @@ -15,7 +15,6 @@ import { init as initHttp } from '../../../public/app/services/http'; import { init as initNotification } from '../../../public/app/services/notification'; import { init as initUiMetric } from '../../../public/app/services/ui_metric'; import { init as initHttpRequests } from './http_requests'; -import { createUiStatsReporter } from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; export const setupEnvironment = () => { chrome.breadcrumbs = { @@ -25,7 +24,7 @@ export const setupEnvironment = () => { initHttp(axios.create({ adapter: axiosXhrAdapter }), (path) => path); initBreadcrumb(() => {}, MANAGEMENT_BREADCRUMB); initNotification(toastNotifications, fatalError); - initUiMetric(createUiStatsReporter); + initUiMetric(() => () => {}); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/legacy/plugins/remote_clusters/public/app/app.js b/x-pack/legacy/plugins/remote_clusters/public/app/app.js index 6fea66ad03c9c4..483b2f5b97e273 100644 --- a/x-pack/legacy/plugins/remote_clusters/public/app/app.js +++ b/x-pack/legacy/plugins/remote_clusters/public/app/app.js @@ -6,21 +6,19 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { Switch, Route, Redirect } from 'react-router-dom'; +import { Switch, Route, Redirect, withRouter } from 'react-router-dom'; import { CRUD_APP_BASE_PATH, UIM_APP_LOAD } from './constants'; import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { RemoteClusterList, RemoteClusterAdd, RemoteClusterEdit } from './sections'; -export class App extends Component { - static contextTypes = { - router: PropTypes.shape({ - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired - }).isRequired - }).isRequired - } +class AppComponent extends Component { + static propTypes = { + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + createHref: PropTypes.func.isRequired, + }).isRequired, + }; constructor(...args) { super(...args); @@ -29,8 +27,11 @@ export class App extends Component { registerRouter() { // Share the router with the app without requiring React or context. - const { router } = this.context; - registerRouter(router); + const { history, location } = this.props; + registerRouter({ + history, + route: { location }, + }); } componentDidMount() { @@ -56,3 +57,5 @@ export class App extends Component { ); } } + +export const App = withRouter(AppComponent); diff --git a/x-pack/legacy/plugins/reporting/common/constants.ts b/x-pack/legacy/plugins/reporting/common/constants.ts index 723320e74bfbd8..03b2d51c7b3965 100644 --- a/x-pack/legacy/plugins/reporting/common/constants.ts +++ b/x-pack/legacy/plugins/reporting/common/constants.ts @@ -50,3 +50,9 @@ export const PNG_JOB_TYPE = 'PNG'; export const CSV_JOB_TYPE = 'csv'; export const CSV_FROM_SAVEDOBJECT_JOB_TYPE = 'csv_from_savedobject'; export const USES_HEADLESS_JOB_TYPES = [PDF_JOB_TYPE, PNG_JOB_TYPE]; + +export const LICENSE_TYPE_TRIAL = 'trial'; +export const LICENSE_TYPE_BASIC = 'basic'; +export const LICENSE_TYPE_STANDARD = 'standard'; +export const LICENSE_TYPE_GOLD = 'gold'; +export const LICENSE_TYPE_PLATINUM = 'platinum'; diff --git a/x-pack/legacy/plugins/reporting/common/export_types_registry.js b/x-pack/legacy/plugins/reporting/common/export_types_registry.js deleted file mode 100644 index 39abd8911e751f..00000000000000 --- a/x-pack/legacy/plugins/reporting/common/export_types_registry.js +++ /dev/null @@ -1,64 +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 { isString } from 'lodash'; - -export class ExportTypesRegistry { - - constructor() { - this._map = new Map(); - } - - register(item) { - if (!isString(item.id)) { - throw new Error(`'item' must have a String 'id' property `); - } - - if (this._map.has(item.id)) { - throw new Error(`'item' with id ${item.id} has already been registered`); - } - - this._map.set(item.id, item); - } - - getAll() { - return this._map.values(); - } - - getSize() { - return this._map.size; - } - - getById(id) { - if (!this._map.has(id)) { - throw new Error(`Unknown id ${id}`); - } - - return this._map.get(id); - } - - get(callback) { - let result; - for (const value of this._map.values()) { - if (!callback(value)) { - continue; - } - - if (result) { - throw new Error('Found multiple items matching predicate.'); - } - - result = value; - } - - if (!result) { - throw new Error('Found no items matching predicate'); - } - - return result; - } - -} diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts index 927cf9ec7d8013..ce51dc2317c79a 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/get_element_position_data.ts @@ -47,5 +47,11 @@ export const getElementPositionAndAttributes = async ( args: [layout.selectors.screenshot, { title: 'data-title', description: 'data-description' }], }); + if (elementsPositionAndAttributes.length === 0) { + throw new Error( + `No shared items containers were found on the page! Reporting requires a container element with the '${layout.selectors.screenshot}' attribute on the page.` + ); + } + return elementsPositionAndAttributes; }; diff --git a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts index 2b4411584d752a..152ef32e331b98 100644 --- a/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/common/lib/screenshots/index.ts @@ -6,8 +6,12 @@ import * as Rx from 'rxjs'; import { first, mergeMap } from 'rxjs/operators'; -import { ServerFacade, CaptureConfig } from '../../../../types'; -import { HeadlessChromiumDriver as HeadlessBrowser } from '../../../../server/browsers/chromium/driver'; +import { + ServerFacade, + CaptureConfig, + HeadlessChromiumDriverFactory, + HeadlessChromiumDriver as HeadlessBrowser, +} from '../../../../types'; import { ElementsPositionAndAttribute, ScreenshotResults, @@ -26,10 +30,12 @@ import { getElementPositionAndAttributes } from './get_element_position_data'; import { getScreenshots } from './get_screenshots'; import { skipTelemetry } from './skip_telemetry'; -export function screenshotsObservableFactory(server: ServerFacade) { +export function screenshotsObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); - const { browserDriverFactory } = server.plugins.reporting!; return function screenshotsObservable({ logger, diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv/index.ts new file mode 100644 index 00000000000000..4f8aeb2be0c993 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/csv/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { + CSV_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { metadata } from './metadata'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { JobParamsDiscoverCsv, JobDocPayloadDiscoverCsv } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsDiscoverCsv, + ESQueueCreateJobFn, + JobDocPayloadDiscoverCsv, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js index 16408b09d5953a..3e35fd970e2c79 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/__tests__/execute_job.js @@ -11,6 +11,8 @@ import nodeCrypto from '@elastic/node-crypto'; import { CancellationToken } from '../../../../common/cancellation_token'; import { FieldFormatsService } from '../../../../../../../../src/legacy/ui/field_formats/mixin/field_formats_service'; +// Reporting uses an unconventional directory structure so the linter marks this as a violation +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { StringFormat } from '../../../../../../../../src/plugins/data/server'; import { executeJobFactory } from '../execute_job'; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv/server/index.ts deleted file mode 100644 index d752cdcd9779d3..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/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 { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { CSV_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js index e9da43aa1dd32d..ac130e4721601c 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js +++ b/x-pack/legacy/plugins/reporting/export_types/csv/server/lib/__tests__/field_format_map.js @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { FieldFormatsService } from '../../../../../../../../../src/legacy/ui/field_formats/mixin/field_formats_service'; +// Reporting uses an unconventional directory structure so the linter marks this as a violation +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BytesFormat, NumberFormat } from '../../../../../../../../../src/plugins/data/server'; import { fieldFormatMapFactory } from '../field_format_map'; diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts index 68ad4a4b491559..4876cea0b1b28b 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/index.ts @@ -4,9 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ +import { + CSV_FROM_SAVEDOBJECT_JOB_TYPE, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ImmediateCreateJobFn, ImmediateExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPanelCsv } from './types'; + /* * These functions are exported to share with the API route handler that * generates csv from saved object immediately on request. */ export { executeJobFactory } from './server/execute_job'; export { createJobFactory } from './server/create_job'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPanelCsv, + ImmediateCreateJobFn, + JobParamsPanelCsv, + ImmediateExecuteFn +> => ({ + ...metadata, + jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, + jobContentExtension: 'csv', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_BASIC, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/index.ts deleted file mode 100644 index b614fd3c681b39..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/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 { CSV_FROM_SAVEDOBJECT_JOB_TYPE } from '../../../common/constants'; -import { ExportTypesRegistry } from '../../../types'; -import { metadata } from '../metadata'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType: CSV_FROM_SAVEDOBJECT_JOB_TYPE, - jobContentExtension: 'csv', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'basic', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts index 081800d1d7b2d3..ba29c3ef1ec3fb 100644 --- a/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts +++ b/x-pack/legacy/plugins/reporting/export_types/csv_from_savedobject/server/lib/generate_csv_search.ts @@ -18,11 +18,15 @@ import { import { getDataSource } from './get_data_source'; import { getFilters } from './get_filters'; import { JobParamsDiscoverCsv } from '../../../csv/types'; + import { esQuery, esFilters, IIndexPattern, Query, + // Reporting uses an unconventional directory structure so the linter marks this as a violation, server files should + // be moved under reporting/server/ + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../../../../src/plugins/data/server'; const getEsQueryConfig = async (config: any) => { diff --git a/x-pack/legacy/plugins/reporting/export_types/png/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/index.ts new file mode 100644 index 00000000000000..bc00bc428f3066 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/png/index.ts @@ -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 { + PNG_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPNG, JobDocPayloadPNG } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPNG, + ESQueueCreateJobFn, + JobDocPayloadPNG, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'PNG', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js index 867d537017f416..267c606449c3a8 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.test.js @@ -68,7 +68,7 @@ test(`passes browserTimezone to generatePng`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const browserTimezone = 'UTC'; await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something', browserTimezone, headers: encryptedHeaders }, cancellationToken); @@ -76,7 +76,7 @@ test(`passes browserTimezone to generatePng`, async () => { }); test(`returns content_type of application/png`, async () => { - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const generatePngObservable = generatePngObservableFactory(); @@ -93,7 +93,7 @@ test(`returns content of generatePng getBuffer base64 encoded`, async () => { const generatePngObservable = generatePngObservableFactory(); generatePngObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob('pngJobId', { relativeUrl: '/app/kibana#/something', timeRange: {}, headers: encryptedHeaders }, cancellationToken); diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts index 6678a83079d312..b289ae45dde678 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/execute_job/index.ts @@ -7,7 +7,12 @@ import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; import { PLUGIN_ID, PNG_JOB_TYPE } from '../../../../common/constants'; -import { ServerFacade, ExecuteJobFactory, ESQueueWorkerExecuteFn } from '../../../../types'; +import { + ServerFacade, + ExecuteJobFactory, + ESQueueWorkerExecuteFn, + HeadlessChromiumDriverFactory, +} from '../../../../types'; import { LevelLogger } from '../../../../server/lib'; import { decryptJobHeaders, @@ -18,10 +23,13 @@ import { import { JobDocPayloadPNG } from '../../types'; import { generatePngObservableFactory } from '../lib/generate_png'; -export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade) { - const generatePngObservable = generatePngObservableFactory(server); +type QueuedPngExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPngExecutorFactory = function executeJobFactoryFn( + server: ServerFacade, + { browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory } +) { + const generatePngObservable = generatePngObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PNG_JOB_TYPE, 'execute']); return function executeJob( diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts deleted file mode 100644 index a569719f02324b..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/index.ts +++ /dev/null @@ -1,23 +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 { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { PNG_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'PNG', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts index 90aeea25db8583..e2b1474515786e 100644 --- a/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts +++ b/x-pack/legacy/plugins/reporting/export_types/png/server/lib/generate_png.ts @@ -7,13 +7,16 @@ import * as Rx from 'rxjs'; import { map } from 'rxjs/operators'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, ConditionalHeaders } from '../../../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; import { PreserveLayout } from '../../../common/layouts/preserve_layout'; import { LayoutParams } from '../../../common/layouts/layout'; -export function generatePngObservableFactory(server: ServerFacade) { - const screenshotsObservable = screenshotsObservableFactory(server); +export function generatePngObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { + const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); return function generatePngObservable( logger: LevelLogger, diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts new file mode 100644 index 00000000000000..99880c1237a7a5 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/index.ts @@ -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 { + PDF_JOB_TYPE as jobType, + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, +} from '../../common/constants'; +import { ExportTypeDefinition, ESQueueCreateJobFn, ESQueueWorkerExecuteFn } from '../../types'; +import { createJobFactory } from './server/create_job'; +import { executeJobFactory } from './server/execute_job'; +import { metadata } from './metadata'; +import { JobParamsPDF, JobDocPayloadPDF } from './types'; + +export const getExportType = (): ExportTypeDefinition< + JobParamsPDF, + ESQueueCreateJobFn, + JobDocPayloadPDF, + ESQueueWorkerExecuteFn +> => ({ + ...metadata, + jobType, + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + createJobFactory, + executeJobFactory, + validLicenses: [ + LICENSE_TYPE_TRIAL, + LICENSE_TYPE_STANDARD, + LICENSE_TYPE_GOLD, + LICENSE_TYPE_PLATINUM, + ], +}); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js index 8084c077ed23f3..6a5c47829fd19f 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.test.js @@ -67,7 +67,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const browserTimezone = 'UTC'; await executeJob('pdfJobId', { objects: [], browserTimezone, headers: encryptedHeaders }, cancellationToken); @@ -84,7 +84,7 @@ test(`passes browserTimezone to generatePdf`, async () => { }); test(`returns content_type of application/pdf`, async () => { - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = generatePdfObservableFactory(); @@ -104,7 +104,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = generatePdfObservableFactory(); generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(testContent))); - const executeJob = executeJobFactory(mockServer); + const executeJob = executeJobFactory(mockServer, { browserDriverFactory: {} }); const encryptedHeaders = await encryptHeaders({}); const { content } = await executeJob('pdfJobId', { objects: [], timeRange: {}, headers: encryptedHeaders }, cancellationToken); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts index 543d5b587906d0..e2b3183464cf24 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/execute_job/index.ts @@ -6,7 +6,12 @@ import * as Rx from 'rxjs'; import { mergeMap, catchError, map, takeUntil } from 'rxjs/operators'; -import { ExecuteJobFactory, ESQueueWorkerExecuteFn, ServerFacade } from '../../../../types'; +import { + ServerFacade, + ExecuteJobFactory, + ESQueueWorkerExecuteFn, + HeadlessChromiumDriverFactory, +} from '../../../../types'; import { JobDocPayloadPDF } from '../../types'; import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; import { LevelLogger } from '../../../../server/lib'; @@ -19,10 +24,13 @@ import { getCustomLogo, } from '../../../common/execute_job/'; -export const executeJobFactory: ExecuteJobFactory> = function executeJobFactoryFn(server: ServerFacade) { - const generatePdfObservable = generatePdfObservableFactory(server); +type QueuedPdfExecutorFactory = ExecuteJobFactory>; + +export const executeJobFactory: QueuedPdfExecutorFactory = function executeJobFactoryFn( + server: ServerFacade, + { browserDriverFactory }: { browserDriverFactory: HeadlessChromiumDriverFactory } +) { + const generatePdfObservable = generatePdfObservableFactory(server, browserDriverFactory); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'execute']); return function executeJob( diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts deleted file mode 100644 index df798a7af23ec8..00000000000000 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/index.ts +++ /dev/null @@ -1,23 +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 { ExportTypesRegistry } from '../../../types'; -import { createJobFactory } from './create_job'; -import { executeJobFactory } from './execute_job'; -import { metadata } from '../metadata'; -import { PDF_JOB_TYPE as jobType } from '../../../common/constants'; - -export function register(registry: ExportTypesRegistry) { - registry.register({ - ...metadata, - jobType, - jobContentEncoding: 'base64', - jobContentExtension: 'pdf', - createJobFactory, - executeJobFactory, - validLicenses: ['trial', 'standard', 'gold', 'platinum'], - }); -} diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts index 1e0245ebd513f1..898a13a2dfe805 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/lib/generate_pdf.ts @@ -8,7 +8,7 @@ import * as Rx from 'rxjs'; import { toArray, mergeMap } from 'rxjs/operators'; import { groupBy } from 'lodash'; import { LevelLogger } from '../../../../server/lib'; -import { ServerFacade, ConditionalHeaders } from '../../../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory, ConditionalHeaders } from '../../../../types'; // @ts-ignore untyped module import { pdf } from './pdf'; import { screenshotsObservableFactory } from '../../../common/lib/screenshots'; @@ -26,8 +26,11 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export function generatePdfObservableFactory(server: ServerFacade) { - const screenshotsObservable = screenshotsObservableFactory(server); +export function generatePdfObservableFactory( + server: ServerFacade, + browserDriverFactory: HeadlessChromiumDriverFactory +) { + const screenshotsObservable = screenshotsObservableFactory(server, browserDriverFactory); const captureConcurrency = 1; return function generatePdfObservable( diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 9add3accd262f2..c0c9e458132f05 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -13,8 +13,7 @@ import { registerRoutes } from './server/routes'; import { LevelLogger, checkLicenseFactory, - createQueueFactory, - exportTypesRegistryFactory, + getExportTypesRegistry, runValidations, } from './server/lib'; import { config as reportingConfig } from './config'; @@ -74,20 +73,23 @@ export const reporting = (kibana: any) => { // TODO: Decouple Hapi: Build a server facade object based on the server to // pass through to the libs. Do not pass server directly async init(server: ServerFacade) { + const exportTypesRegistry = getExportTypesRegistry(); + let isCollectorReady = false; // Register a function with server to manage the collection of usage stats const { usageCollection } = server.newPlatform.setup.plugins; - registerReportingUsageCollector(usageCollection, server, () => isCollectorReady); + registerReportingUsageCollector( + usageCollection, + server, + () => isCollectorReady, + exportTypesRegistry + ); const logger = LevelLogger.createForServer(server, [PLUGIN_ID]); - const [exportTypesRegistry, browserFactory] = await Promise.all([ - exportTypesRegistryFactory(server), - createBrowserDriverFactory(server), - ]); - server.expose('exportTypesRegistry', exportTypesRegistry); + const browserDriverFactory = await createBrowserDriverFactory(server); logConfiguration(server, logger); - runValidations(server, logger, browserFactory); + runValidations(server, logger, browserDriverFactory); const { xpack_main: xpackMainPlugin } = server.plugins; mirrorPluginStatus(xpackMainPlugin, this); @@ -101,11 +103,8 @@ export const reporting = (kibana: any) => { // Post initialization of the above code, the collector is now ready to fetch its data isCollectorReady = true; - server.expose('browserDriverFactory', browserFactory); - server.expose('queue', createQueueFactory(server)); - // Reporting routes - registerRoutes(server, logger); + registerRoutes(server, exportTypesRegistry, browserDriverFactory, logger); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/common/__tests__/export_types_registry.js b/x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js similarity index 100% rename from x-pack/legacy/plugins/reporting/common/__tests__/export_types_registry.js rename to x-pack/legacy/plugins/reporting/server/lib/__tests__/export_types_registry.js diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts index 174c6d587e523e..5cf760250ec0e3 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_queue.ts @@ -5,7 +5,12 @@ */ import { PLUGIN_ID } from '../../common/constants'; -import { ServerFacade, QueueConfig } from '../../types'; +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + QueueConfig, +} from '../../types'; // @ts-ignore import { Esqueue } from './esqueue'; import { createWorkerFactory } from './create_worker'; @@ -13,7 +18,15 @@ import { LevelLogger } from './level_logger'; // @ts-ignore import { createTaggedLogger } from './create_tagged_logger'; // TODO remove createTaggedLogger once esqueue is removed -export function createQueueFactory(server: ServerFacade): Esqueue { +interface CreateQueueFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + browserDriverFactory: HeadlessChromiumDriverFactory; +} + +export function createQueueFactory( + server: ServerFacade, + { exportTypesRegistry, browserDriverFactory }: CreateQueueFactoryOpts +): Esqueue { const queueConfig: QueueConfig = server.config().get('xpack.reporting.queue'); const index = server.config().get('xpack.reporting.index'); @@ -29,7 +42,7 @@ export function createQueueFactory(server: ServerFacade): Esqueue { if (queueConfig.pollEnabled) { // create workers to poll the index for idle jobs waiting to be claimed and executed - const createWorker = createWorkerFactory(server); + const createWorker = createWorkerFactory(server, { exportTypesRegistry, browserDriverFactory }); createWorker(queue); } else { const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'create_queue']); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts index afad8f096a8bb9..8f843752491ecb 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.test.ts @@ -5,7 +5,8 @@ */ import * as sinon from 'sinon'; -import { ServerFacade } from '../../types'; +import { ServerFacade, HeadlessChromiumDriverFactory } from '../../types'; +import { ExportTypesRegistry } from './export_types_registry'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -22,16 +23,17 @@ configGetStub.withArgs('server.uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr const executeJobFactoryStub = sinon.stub(); -const getMockServer = ( - exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] -): ServerFacade => { +const getMockServer = (): ServerFacade => { return ({ log: sinon.stub(), - expose: sinon.stub(), config: () => ({ get: configGetStub }), - plugins: { reporting: { exportTypesRegistry: { getAll: () => exportTypes } } }, } as unknown) as ServerFacade; }; +const getMockExportTypesRegistry = ( + exportTypes: any[] = [{ executeJobFactory: executeJobFactoryStub }] +) => ({ + getAll: () => exportTypes, +}); describe('Create Worker', () => { let queue: Esqueue; @@ -44,7 +46,11 @@ describe('Create Worker', () => { }); test('Creates a single Esqueue worker for Reporting', async () => { - const createWorker = createWorkerFactory(getMockServer()); + const exportTypesRegistry = getMockExportTypesRegistry(); + const createWorker = createWorkerFactory(getMockServer(), { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); @@ -68,15 +74,17 @@ Object { }); test('Creates a single Esqueue worker for Reporting, even if there are multiple export types', async () => { - const createWorker = createWorkerFactory( - getMockServer([ - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - { executeJobFactory: executeJobFactoryStub }, - ]) - ); + const exportTypesRegistry = getMockExportTypesRegistry([ + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + { executeJobFactory: executeJobFactoryStub }, + ]); + const createWorker = createWorkerFactory(getMockServer(), { + exportTypesRegistry: exportTypesRegistry as ExportTypesRegistry, + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); createWorker(queue); diff --git a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts index 01f59099a1d999..1326e411b6c5c7 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/create_worker.ts @@ -5,6 +5,7 @@ */ import { PLUGIN_ID } from '../../common/constants'; +import { ExportTypesRegistry, HeadlessChromiumDriverFactory } from '../../types'; import { CancellationToken } from '../../common/cancellation_token'; import { ESQueueInstance, @@ -21,14 +22,21 @@ import { import { events as esqueueEvents } from './esqueue'; import { LevelLogger } from './level_logger'; -export function createWorkerFactory(server: ServerFacade) { +interface CreateWorkerFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + browserDriverFactory: HeadlessChromiumDriverFactory; +} + +export function createWorkerFactory( + server: ServerFacade, + { exportTypesRegistry, browserDriverFactory }: CreateWorkerFactoryOpts +) { type JobDocPayloadType = JobDocPayload; const config = server.config(); const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'queue-worker']); const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); const kibanaName: string = config.get('server.name'); const kibanaId: string = config.get('server.uuid'); - const { exportTypesRegistry } = server.plugins.reporting!; // Once more document types are added, this will need to be passed in return function createWorker(queue: ESQueueInstance) { @@ -41,8 +49,9 @@ export function createWorkerFactory(server: ServerFacade) { for (const exportType of exportTypesRegistry.getAll() as Array< ExportTypeDefinition >) { - const executeJobFactory = exportType.executeJobFactory(server); - jobExecutors.set(exportType.jobType, executeJobFactory); + // TODO: the executeJobFn should be unwrapped in the register method of the export types registry + const jobExecutor = exportType.executeJobFactory(server, { browserDriverFactory }); + jobExecutors.set(exportType.jobType, jobExecutor); } const workerFn = (jobSource: JobSource, ...workerRestArgs: any[]) => { diff --git a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts index a8cefa3fdc49bb..2d044ab31a160e 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/enqueue_job.ts @@ -8,12 +8,14 @@ import { get } from 'lodash'; // @ts-ignore import { events as esqueueEvents } from './esqueue'; import { + EnqueueJobFn, ESQueueCreateJobFn, ImmediateCreateJobFn, Job, ServerFacade, RequestFacade, Logger, + ExportTypesRegistry, CaptureConfig, QueueConfig, ConditionalHeaders, @@ -26,13 +28,20 @@ interface ConfirmedJob { _primary_term: number; } -export function enqueueJobFactory(server: ServerFacade) { +interface EnqueueJobFactoryOpts { + exportTypesRegistry: ExportTypesRegistry; + esqueue: any; +} + +export function enqueueJobFactory( + server: ServerFacade, + { exportTypesRegistry, esqueue }: EnqueueJobFactoryOpts +): EnqueueJobFn { const config = server.config(); const captureConfig: CaptureConfig = config.get('xpack.reporting.capture'); const browserType = captureConfig.browser.type; const maxAttempts = captureConfig.maxAttempts; const queueConfig: QueueConfig = config.get('xpack.reporting.queue'); - const { exportTypesRegistry, queue: jobQueue } = server.plugins.reporting!; return async function enqueueJob( parentLogger: Logger, @@ -46,6 +55,12 @@ export function enqueueJobFactory(server: ServerFacade) { const logger = parentLogger.clone(['queue-job']); const exportType = exportTypesRegistry.getById(exportTypeId); + + if (exportType == null) { + throw new Error(`Export type ${exportTypeId} does not exist in the registry!`); + } + + // TODO: the createJobFn should be unwrapped in the register method of the export types registry const createJob = exportType.createJobFactory(server) as CreateJobFn; const payload = await createJob(jobParams, headers, request); @@ -57,7 +72,7 @@ export function enqueueJobFactory(server: ServerFacade) { }; return new Promise((resolve, reject) => { - const job = jobQueue.addJob(exportType.jobType, payload, options); + const job = esqueue.addJob(exportType.jobType, payload, options); job.on(esqueueEvents.EVENT_JOB_CREATED, (createdJob: ConfirmedJob) => { if (createdJob.id === job.id) { diff --git a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts b/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts index af1457ab52fe20..d553cc07ae3ef8 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/export_types_registry.ts @@ -4,40 +4,110 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve as pathResolve } from 'path'; -import glob from 'glob'; -import { ServerFacade } from '../../types'; -import { PLUGIN_ID } from '../../common/constants'; -import { oncePerServer } from './once_per_server'; -import { LevelLogger } from './level_logger'; -// @ts-ignore untype module TODO -import { ExportTypesRegistry } from '../../common/export_types_registry'; - -function scan(pattern: string) { - return new Promise((resolve, reject) => { - glob(pattern, {}, (err, files) => { - if (err) { - return reject(err); +import memoizeOne from 'memoize-one'; +import { isString } from 'lodash'; +import { getExportType as getTypeCsv } from '../../export_types/csv'; +import { getExportType as getTypeCsvFromSavedObject } from '../../export_types/csv_from_savedobject'; +import { getExportType as getTypePng } from '../../export_types/png'; +import { getExportType as getTypePrintablePdf } from '../../export_types/printable_pdf'; +import { ExportTypeDefinition } from '../../types'; + +type GetCallbackFn = ( + item: ExportTypeDefinition +) => boolean; +// => ExportTypeDefinition + +export class ExportTypesRegistry { + private _map: Map> = new Map(); + + constructor() {} + + register( + item: ExportTypeDefinition + ): void { + if (!isString(item.id)) { + throw new Error(`'item' must have a String 'id' property `); + } + + if (this._map.has(item.id)) { + throw new Error(`'item' with id ${item.id} has already been registered`); + } + + // TODO: Unwrap the execute function from the item's executeJobFactory + // Move that work out of server/lib/create_worker to reduce dependence on ESQueue + this._map.set(item.id, item); + } + + getAll() { + return Array.from(this._map.values()); + } + + getSize() { + return this._map.size; + } + + getById( + id: string + ): ExportTypeDefinition { + if (!this._map.has(id)) { + throw new Error(`Unknown id ${id}`); + } + + return this._map.get(id) as ExportTypeDefinition< + JobParamsType, + CreateJobFnType, + JobPayloadType, + ExecuteJobFnType + >; + } + + get( + findType: GetCallbackFn + ): ExportTypeDefinition { + let result; + for (const value of this._map.values()) { + if (!findType(value)) { + continue; // try next value } + const foundResult: ExportTypeDefinition< + JobParamsType, + CreateJobFnType, + JobPayloadType, + ExecuteJobFnType + > = value; - resolve(files); - }); - }); -} + if (result) { + throw new Error('Found multiple items matching predicate.'); + } + + result = foundResult; + } -const pattern = pathResolve(__dirname, '../../export_types/*/server/index.[jt]s'); -async function exportTypesRegistryFn(server: ServerFacade) { - const logger = LevelLogger.createForServer(server, [PLUGIN_ID, 'exportTypes']); - const exportTypesRegistry = new ExportTypesRegistry(); - const files: string[] = (await scan(pattern)) as string[]; + if (!result) { + throw new Error('Found no items matching predicate'); + } + + return result; + } +} - files.forEach(file => { - logger.debug(`Found exportType at ${file}`); +function getExportTypesRegistryFn(): ExportTypesRegistry { + const registry = new ExportTypesRegistry(); - const { register } = require(file); // eslint-disable-line @typescript-eslint/no-var-requires - register(exportTypesRegistry); + /* this replaces the previously async method of registering export types, + * where this would run a directory scan and types would be registered via + * discovery */ + const getTypeFns: Array<() => ExportTypeDefinition> = [ + getTypeCsv, + getTypeCsvFromSavedObject, + getTypePng, + getTypePrintablePdf, + ]; + getTypeFns.forEach(getType => { + registry.register(getType()); }); - return exportTypesRegistry; + return registry; } -export const exportTypesRegistryFactory = oncePerServer(exportTypesRegistryFn); +// FIXME: is this the best way to return a singleton? +export const getExportTypesRegistry = memoizeOne(getExportTypesRegistryFn); diff --git a/x-pack/legacy/plugins/reporting/server/lib/index.ts b/x-pack/legacy/plugins/reporting/server/lib/index.ts index b11f7bd95d9efd..50d1a276b6b5d0 100644 --- a/x-pack/legacy/plugins/reporting/server/lib/index.ts +++ b/x-pack/legacy/plugins/reporting/server/lib/index.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { exportTypesRegistryFactory } from './export_types_registry'; +export { getExportTypesRegistry } from './export_types_registry'; // @ts-ignore untyped module export { checkLicenseFactory } from './check_license'; export { LevelLogger } from './level_logger'; -export { createQueueFactory } from './create_queue'; export { cryptoFactory } from './crypto'; export { oncePerServer } from './once_per_server'; export { runValidations } from './validate'; +export { createQueueFactory } from './create_queue'; +export { enqueueJobFactory } from './enqueue_job'; diff --git a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts index 8b0dd1a6e7c4f6..bc96c27f64c103 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/generate_from_savedobject_immediate.ts @@ -10,6 +10,7 @@ import { ServerFacade, RequestFacade, ResponseFacade, + HeadlessChromiumDriverFactory, ReportingResponseToolkit, Logger, JobDocOutputExecuted, @@ -45,8 +46,17 @@ export function registerGenerateCsvFromSavedObjectImmediate( handler: async (request: RequestFacade, h: ReportingResponseToolkit) => { const logger = parentLogger.clone(['savedobject-csv']); const jobParams = getJobParamsFromRequest(request, { isImmediate: true }); + + /* TODO these functions should be made available in the export types registry: + * + * const { createJobFn, executeJobFn } = exportTypesRegistry.getById(CSV_FROM_SAVEDOBJECT_JOB_TYPE) + * + * Calling an execute job factory requires passing a browserDriverFactory option, so we should not call the factory from here + */ const createJobFn = createJobFactory(server); - const executeJobFn = executeJobFactory(server); + const executeJobFn = executeJobFactory(server, { + browserDriverFactory: {} as HeadlessChromiumDriverFactory, + }); const jobDocPayload: JobDocPayloadPanelCsv = await createJobFn( jobParams, request.headers, diff --git a/x-pack/legacy/plugins/reporting/server/routes/generation.ts b/x-pack/legacy/plugins/reporting/server/routes/generation.ts new file mode 100644 index 00000000000000..7bed7bc5773e45 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/routes/generation.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 boom from 'boom'; +import { API_BASE_URL } from '../../common/constants'; +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + RequestFacade, + ReportingResponseToolkit, + Logger, +} from '../../types'; +import { registerGenerateFromJobParams } from './generate_from_jobparams'; +import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; +import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; +import { registerLegacy } from './legacy'; +import { createQueueFactory, enqueueJobFactory } from '../lib'; + +export function registerJobGenerationRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + browserDriverFactory: HeadlessChromiumDriverFactory, + logger: Logger +) { + const config = server.config(); + const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; + // @ts-ignore TODO + const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); + + const esqueue = createQueueFactory(server, { exportTypesRegistry, browserDriverFactory }); + const enqueueJob = enqueueJobFactory(server, { exportTypesRegistry, esqueue }); + + /* + * Generates enqueued job details to use in responses + */ + async function handler( + exportTypeId: string, + jobParams: object, + request: RequestFacade, + h: ReportingResponseToolkit + ) { + const user = request.pre.user; + const headers = request.headers; + + const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request); + + // return the queue's job information + const jobJson = job.toJSON(); + + return h + .response({ + path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`, + job: jobJson, + }) + .type('application/json'); + } + + function handleError(exportTypeId: string, err: Error) { + if (err instanceof esErrors['401']) { + return boom.unauthorized(`Sorry, you aren't authenticated`); + } + if (err instanceof esErrors['403']) { + return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); + } + if (err instanceof esErrors['404']) { + return boom.boomify(err, { statusCode: 404 }); + } + return err; + } + + registerGenerateFromJobParams(server, handler, handleError); + registerLegacy(server, handler, handleError); + + // Register beta panel-action download-related API's + if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { + registerGenerateCsvFromSavedObject(server, handler, handleError); + registerGenerateCsvFromSavedObjectImmediate(server, logger); + } +} diff --git a/x-pack/legacy/plugins/reporting/server/routes/index.ts b/x-pack/legacy/plugins/reporting/server/routes/index.ts index c48a37a36812e4..da664dcb91ae44 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/index.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/index.ts @@ -4,69 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import boom from 'boom'; -import { API_BASE_URL } from '../../common/constants'; -import { ServerFacade, RequestFacade, ReportingResponseToolkit, Logger } from '../../types'; -import { enqueueJobFactory } from '../lib/enqueue_job'; -import { registerGenerateFromJobParams } from './generate_from_jobparams'; -import { registerGenerateCsvFromSavedObject } from './generate_from_savedobject'; -import { registerGenerateCsvFromSavedObjectImmediate } from './generate_from_savedobject_immediate'; -import { registerJobs } from './jobs'; -import { registerLegacy } from './legacy'; - -export function registerRoutes(server: ServerFacade, logger: Logger) { - const config = server.config(); - const DOWNLOAD_BASE_URL = config.get('server.basePath') + `${API_BASE_URL}/jobs/download`; - // @ts-ignore TODO - const { errors: esErrors } = server.plugins.elasticsearch.getCluster('admin'); - const enqueueJob = enqueueJobFactory(server); - - /* - * Generates enqueued job details to use in responses - */ - async function handler( - exportTypeId: string, - jobParams: object, - request: RequestFacade, - h: ReportingResponseToolkit - ) { - const user = request.pre.user; - const headers = request.headers; - - const job = await enqueueJob(logger, exportTypeId, jobParams, user, headers, request); - - // return the queue's job information - const jobJson = job.toJSON(); - - return h - .response({ - path: `${DOWNLOAD_BASE_URL}/${jobJson.id}`, - job: jobJson, - }) - .type('application/json'); - } - - function handleError(exportTypeId: string, err: Error) { - if (err instanceof esErrors['401']) { - return boom.unauthorized(`Sorry, you aren't authenticated`); - } - if (err instanceof esErrors['403']) { - return boom.forbidden(`Sorry, you are not authorized to create ${exportTypeId} reports`); - } - if (err instanceof esErrors['404']) { - return boom.boomify(err, { statusCode: 404 }); - } - return err; - } - - registerGenerateFromJobParams(server, handler, handleError); - registerLegacy(server, handler, handleError); - - // Register beta panel-action download-related API's - if (config.get('xpack.reporting.csv.enablePanelActionDownload')) { - registerGenerateCsvFromSavedObject(server, handler, handleError); - registerGenerateCsvFromSavedObjectImmediate(server, logger); - } - - registerJobs(server); +import { + ServerFacade, + ExportTypesRegistry, + HeadlessChromiumDriverFactory, + Logger, +} from '../../types'; +import { registerJobGenerationRoutes } from './generation'; +import { registerJobInfoRoutes } from './jobs'; + +export function registerRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + browserDriverFactory: HeadlessChromiumDriverFactory, + logger: Logger +) { + registerJobGenerationRoutes(server, exportTypesRegistry, browserDriverFactory, logger); + registerJobInfoRoutes(server, exportTypesRegistry, logger); } diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js index 2d1f48dd790a0b..c4d4f6e42c9cb7 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.test.js @@ -6,8 +6,8 @@ import Hapi from 'hapi'; import { difference, memoize } from 'lodash'; -import { registerJobs } from './jobs'; -import { ExportTypesRegistry } from '../../common/export_types_registry'; +import { registerJobInfoRoutes } from './jobs'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; jest.mock('./lib/authorized_user_pre_routing', () => { return { authorizedUserPreRoutingFactory: () => () => ({}) @@ -19,13 +19,17 @@ jest.mock('./lib/reporting_feature_pre_routing', () => { }; }); - let mockServer; +let exportTypesRegistry; +const mockLogger = { + error: jest.fn(), + debug: jest.fn(), +}; beforeEach(() => { mockServer = new Hapi.Server({ debug: false, port: 8080, routes: { log: { collect: true } } }); mockServer.config = memoize(() => ({ get: jest.fn() })); - const exportTypesRegistry = new ExportTypesRegistry(); + exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ id: 'unencoded', jobType: 'unencodedJobType', @@ -44,9 +48,6 @@ beforeEach(() => { callWithRequest: jest.fn(), callWithInternalUser: jest.fn(), })) - }, - reporting: { - exportTypesRegistry } }; }); @@ -63,7 +64,7 @@ test(`returns 404 if job not found`, async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits())); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -79,7 +80,7 @@ test(`returns 401 if not valid job type`, async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'invalidJobType' }))); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -91,12 +92,11 @@ test(`returns 401 if not valid job type`, async () => { }); describe(`when job is incomplete`, () => { - const getIncompleteResponse = async () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(getHits({ jobtype: 'unencodedJobType', status: 'pending' }))); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -133,7 +133,7 @@ describe(`when job is failed`, () => { mockServer.plugins.elasticsearch.getCluster('admin') .callWithInternalUser.mockReturnValue(Promise.resolve(hits)); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', @@ -178,7 +178,7 @@ describe(`when job is completed`, () => { }); mockServer.plugins.elasticsearch.getCluster('admin').callWithInternalUser.mockReturnValue(Promise.resolve(hits)); - registerJobs(mockServer); + registerJobInfoRoutes(mockServer, exportTypesRegistry, mockLogger); const request = { method: 'GET', diff --git a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts index 71d9f0d3ae13bb..fd5014911d262a 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/jobs.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/jobs.ts @@ -8,6 +8,8 @@ import boom from 'boom'; import { API_BASE_URL } from '../../common/constants'; import { ServerFacade, + ExportTypesRegistry, + Logger, RequestFacade, ReportingResponseToolkit, JobDocOutput, @@ -24,7 +26,11 @@ import { const MAIN_ENTRY = `${API_BASE_URL}/jobs`; -export function registerJobs(server: ServerFacade) { +export function registerJobInfoRoutes( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry, + logger: Logger +) { const jobsQuery = jobsQueryFactory(server); const getRouteConfig = getRouteConfigFactoryManagementPre(server); const getRouteConfigDownload = getRouteConfigFactoryDownloadPre(server); @@ -119,7 +125,7 @@ export function registerJobs(server: ServerFacade) { }); // trigger a download of the output from a job - const jobResponseHandler = jobResponseHandlerFactory(server); + const jobResponseHandler = jobResponseHandlerFactory(server, exportTypesRegistry); server.route({ path: `${MAIN_ENTRY}/download/{docId}`, method: 'GET', @@ -136,13 +142,15 @@ export function registerJobs(server: ServerFacade) { const { statusCode } = response; if (statusCode !== 200) { - const logLevel = statusCode === 500 ? 'error' : 'debug'; - server.log( - [logLevel, 'reporting', 'download'], - `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( - response.source - )}]` - ); + if (statusCode === 500) { + logger.error(`Report ${docId} has failed: ${JSON.stringify(response.source)}`); + } else { + logger.debug( + `Report ${docId} has non-OK status: [${statusCode}] Reason: [${JSON.stringify( + response.source + )}]` + ); + } } if (!response.isBoom) { diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts index a69c19c006b615..c3a30f9dda4543 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -9,6 +9,7 @@ import * as _ from 'lodash'; import contentDisposition from 'content-disposition'; import { ServerFacade, + ExportTypesRegistry, ExportTypeDefinition, JobDocExecuted, JobDocOutputExecuted, @@ -40,9 +41,10 @@ const getReportingHeaders = (output: JobDocOutputExecuted, exportType: ExportTyp return metaDataHeaders; }; -export function getDocumentPayloadFactory(server: ServerFacade) { - const exportTypesRegistry = server.plugins.reporting!.exportTypesRegistry; - +export function getDocumentPayloadFactory( + server: ServerFacade, + exportTypesRegistry: ExportTypesRegistry +) { function encodeContent(content: string | null, exportType: ExportTypeType) { switch (exportType.jobContentEncoding) { case 'base64': diff --git a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js index 758c50816c381e..6bc370506a2557 100644 --- a/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js +++ b/x-pack/legacy/plugins/reporting/server/routes/lib/job_response_handler.js @@ -9,9 +9,9 @@ import { jobsQueryFactory } from '../../lib/jobs_query'; import { WHITELISTED_JOB_CONTENT_TYPES } from '../../../common/constants'; import { getDocumentPayloadFactory } from './get_document_payload'; -export function jobResponseHandlerFactory(server) { +export function jobResponseHandlerFactory(server, exportTypesRegistry) { const jobsQuery = jobsQueryFactory(server); - const getDocumentPayload = getDocumentPayloadFactory(server); + const getDocumentPayload = getDocumentPayloadFactory(server, exportTypesRegistry); return function jobResponseHandler(validJobTypes, user, h, params, opts = {}) { const { docId } = params; diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js b/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts similarity index 61% rename from x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js rename to x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts index a1949b21aa0860..7874f67ef4c0c7 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.js +++ b/x-pack/legacy/plugins/reporting/server/usage/get_export_type_handler.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { exportTypesRegistryFactory } from '../lib/export_types_registry'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; +import { ExportTypesRegistry } from '../lib/export_types_registry'; /* * Gets a handle to the Reporting export types registry and returns a few * functions for examining them - * @param {Object} server: Kibana server * @return {Object} export type handler */ -export async function getExportTypesHandler(server) { - const exportTypesRegistry = await exportTypesRegistryFactory(server); - +export function getExportTypesHandler(exportTypesRegistry: ExportTypesRegistry) { return { /* * Based on the X-Pack license and which export types are available, @@ -23,12 +21,17 @@ export async function getExportTypesHandler(server) { * @param {Object} xpackInfo: xpack_main plugin info object * @return {Object} availability of each export type */ - getAvailability(xpackInfo) { - const exportTypesAvailability = {}; + getAvailability(xpackInfo: XPackMainPlugin['info']) { + const exportTypesAvailability: { [exportType: string]: boolean } = {}; const xpackInfoAvailable = xpackInfo && xpackInfo.isAvailable(); - const licenseType = xpackInfo.license.getType(); - for(const exportType of exportTypesRegistry.getAll()) { - exportTypesAvailability[exportType.jobType] = xpackInfoAvailable ? exportType.validLicenses.includes(licenseType) : false; + const licenseType: string | undefined = xpackInfo.license.getType(); + if (!licenseType) { + throw new Error('No license type returned from XPackMainPlugin#info!'); + } + for (const exportType of exportTypesRegistry.getAll()) { + exportTypesAvailability[exportType.jobType] = xpackInfoAvailable + ? exportType.validLicenses.includes(licenseType) + : false; } return exportTypesAvailability; @@ -39,6 +42,6 @@ export async function getExportTypesHandler(server) { */ getNumExportTypes() { return exportTypesRegistry.getSize(); - } + }, }; } diff --git a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts index 0c85d39ae55d3e..bd2d0cb835a790 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { ServerFacade, ESCallCluster } from '../../types'; +import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types'; import { AggregationBuckets, AggregationResults, @@ -16,7 +16,6 @@ import { RangeStats, } from './types'; import { decorateRangeStats } from './decorate_range_stats'; -// @ts-ignore untyped module import { getExportTypesHandler } from './get_export_type_handler'; const JOB_TYPES_KEY = 'jobTypes'; @@ -101,7 +100,11 @@ async function handleResponse( }; } -export async function getReportingUsage(server: ServerFacade, callCluster: ESCallCluster) { +export async function getReportingUsage( + server: ServerFacade, + callCluster: ESCallCluster, + exportTypesRegistry: ExportTypesRegistry +) { const config = server.config(); const reportingIndex = config.get('xpack.reporting.index'); @@ -138,13 +141,13 @@ export async function getReportingUsage(server: ServerFacade, callCluster: ESCal return callCluster('search', params) .then((response: AggregationResults) => handleResponse(server, response)) - .then(async (usage: RangeStatSets) => { + .then((usage: RangeStatSets) => { // Allow this to explicitly throw an exception if/when this config is deprecated, // because we shouldn't collect browserType in that case! const browserType = config.get('xpack.reporting.capture.browser.type'); const xpackInfo = server.plugins.xpack_main.info; - const exportTypesHandler = await getExportTypesHandler(server); + const exportTypesHandler = getExportTypesHandler(exportTypesRegistry); const availability = exportTypesHandler.getAvailability(xpackInfo) as FeatureAvailabilityMap; const { lastDay, last7Days, ...all } = usage; diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js index f23f6798651463..f761f0d2d270b2 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.test.js @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import sinon from 'sinon'; +import { getExportTypesRegistry } from '../lib/export_types_registry'; import { getReportingUsageCollector } from './reporting_usage_collector'; +const exportTypesRegistry = getExportTypesRegistry(); + function getMockUsageCollection() { class MockUsageCollector { constructor(_server, { fetch }) { @@ -40,7 +43,6 @@ function getServerMock(customization) { }, }, }, - expose: () => {}, log: () => {}, config: () => ({ get: key => { @@ -67,8 +69,13 @@ describe('license checks', () => { .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithBasicLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -93,8 +100,13 @@ describe('license checks', () => { .returns('none'); const callClusterMock = jest.fn(() => Promise.resolve(getResponseMock())); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithNoLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithNoLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -121,9 +133,11 @@ describe('license checks', () => { const usageCollection = getMockUsageCollection(); const { fetch: getReportingUsage } = getReportingUsageCollector( usageCollection, - serverWithPlatinumLicenseMock + serverWithPlatinumLicenseMock, + () => {}, + exportTypesRegistry ); - usageStats = await getReportingUsage(callClusterMock); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -148,8 +162,13 @@ describe('license checks', () => { .returns('basic'); const callClusterMock = jest.fn(() => Promise.resolve({})); const usageCollection = getMockUsageCollection(); - const { fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithBasicLicenseMock); - usageStats = await getReportingUsage(callClusterMock); + const { fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithBasicLicenseMock, + () => {}, + exportTypesRegistry + ); + usageStats = await getReportingUsage(callClusterMock, exportTypesRegistry); }); test('sets enables to true', async () => { @@ -170,7 +189,12 @@ describe('data modeling', () => { serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon .stub() .returns('platinum'); - ({ fetch: getReportingUsage } = getReportingUsageCollector(usageCollection, serverWithPlatinumLicenseMock)); + ({ fetch: getReportingUsage } = getReportingUsageCollector( + usageCollection, + serverWithPlatinumLicenseMock, + () => {}, + exportTypesRegistry + )); }); test('with normal looking usage data', async () => { @@ -295,6 +319,7 @@ describe('data modeling', () => { }) ) ); + const usageStats = await getReportingUsage(callClusterMock); expect(usageStats).toMatchInlineSnapshot(` Object { diff --git a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts index 0a7ef0a1944344..40cf315a78cbb5 100644 --- a/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/legacy/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -7,7 +7,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; // @ts-ignore untyped module import { KIBANA_STATS_TYPE_MONITORING } from '../../../monitoring/common/constants'; -import { ServerFacade, ESCallCluster } from '../../types'; +import { ServerFacade, ExportTypesRegistry, ESCallCluster } from '../../types'; import { KIBANA_REPORTING_TYPE } from '../../common/constants'; import { getReportingUsage } from './get_reporting_usage'; import { RangeStats } from './types'; @@ -19,12 +19,14 @@ import { RangeStats } from './types'; export function getReportingUsageCollector( usageCollection: UsageCollectionSetup, server: ServerFacade, - isReady: () => boolean + isReady: () => boolean, + exportTypesRegistry: ExportTypesRegistry ) { return usageCollection.makeUsageCollector({ type: KIBANA_REPORTING_TYPE, isReady, - fetch: (callCluster: ESCallCluster) => getReportingUsage(server, callCluster), + fetch: (callCluster: ESCallCluster) => + getReportingUsage(server, callCluster, exportTypesRegistry), /* * Format the response data into a model for internal upload @@ -49,8 +51,14 @@ export function getReportingUsageCollector( export function registerReportingUsageCollector( usageCollection: UsageCollectionSetup, server: ServerFacade, - isReady: () => boolean + isReady: () => boolean, + exportTypesRegistry: ExportTypesRegistry ) { - const collector = getReportingUsageCollector(usageCollection, server, isReady); + const collector = getReportingUsageCollector( + usageCollection, + server, + isReady, + exportTypesRegistry + ); usageCollection.registerCollector(collector); } diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index afba9f3e178382..c17b969d5d7fac 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -7,15 +7,18 @@ import { ResponseObject } from 'hapi'; import { EventEmitter } from 'events'; import { Legacy } from 'kibana'; -import { XPackMainPlugin } from '../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../xpack_main/server/xpack_main'; import { ElasticsearchPlugin, CallCluster, } from '../../../../src/legacy/core_plugins/elasticsearch'; import { CancellationToken } from './common/cancellation_token'; +import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; +export type ReportingPlugin = object; // For Plugin contract + export type Job = EventEmitter & { id: string; toJSON: () => { @@ -23,21 +26,6 @@ export type Job = EventEmitter & { }; }; -export interface ReportingPlugin { - queue: { - addJob: (type: string, payload: PayloadType, options: object) => Job; - }; - // TODO: convert exportTypesRegistry to TS - exportTypesRegistry: { - getById: (id: string) => ExportTypeDefinition; - getAll: () => Array>; - get: ( - callback: (item: ExportTypeDefinition) => boolean - ) => ExportTypeDefinition; - }; - browserDriverFactory: HeadlessChromiumDriverFactory; -} - export interface ReportingConfigOptions { browser: BrowserConfig; poll: { @@ -88,7 +76,6 @@ export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; export type ServerFacade = Legacy.Server & { plugins: { - reporting?: ReportingPlugin; xpack_main?: XPackMainPlugin & { status?: any; }; @@ -107,6 +94,15 @@ interface ReportingRequest { }; } +export type EnqueueJobFn = ( + parentLogger: LevelLogger, + exportTypeId: string, + jobParams: JobParamsType, + user: string, + headers: Record, + request: RequestFacade +) => Promise; + export type RequestFacade = ReportingRequest & Legacy.Request; export type ResponseFacade = ResponseObject & { @@ -246,6 +242,10 @@ export interface JobDocOutputExecuted { size: number; } +export interface ESQueue { + addJob: (type: string, payload: object, options: object) => Job; +} + export interface ESQueueWorker { on: (event: string, handler: any) => void; } @@ -304,7 +304,12 @@ export interface ESQueueInstance { } export type CreateJobFactory = (server: ServerFacade) => CreateJobFnType; -export type ExecuteJobFactory = (server: ServerFacade) => ExecuteJobFnType; +export type ExecuteJobFactory = ( + server: ServerFacade, + opts: { + browserDriverFactory: HeadlessChromiumDriverFactory; + } +) => ExecuteJobFnType; export interface ExportTypeDefinition< JobParamsType, @@ -322,21 +327,13 @@ export interface ExportTypeDefinition< validLicenses: string[]; } -export interface ExportTypesRegistry { - register: ( - exportTypeDefinition: ExportTypeDefinition< - JobParamsType, - CreateJobFnType, - JobPayloadType, - ExecuteJobFnType - > - ) => void; -} - +export { ExportTypesRegistry } from './server/lib/export_types_registry'; +export { HeadlessChromiumDriver } from './server/browsers/chromium/driver'; +export { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; export { CancellationToken } from './common/cancellation_token'; // Prefer to import this type using: `import { LevelLogger } from 'relative/path/server/lib';` -export { LevelLogger as Logger } from './server/lib/level_logger'; +export { LevelLogger as Logger }; export interface AbsoluteURLFactoryOptions { defaultBasePath: string; diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/app.js b/x-pack/legacy/plugins/rollup/public/crud_app/app.js index 0e42194097492d..c9f67afd830ecf 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/app.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/app.js @@ -6,22 +6,20 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import { HashRouter, Switch, Route, Redirect, withRouter } from 'react-router-dom'; import { UIM_APP_LOAD } from '../../common'; import { CRUD_APP_BASE_PATH } from './constants'; import { registerRouter, setUserHasLeftApp, trackUiMetric, METRIC_TYPE } from './services'; import { JobList, JobCreate } from './sections'; -class ShareRouter extends Component { - static contextTypes = { - router: PropTypes.shape({ - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired - }).isRequired - }).isRequired - } +class ShareRouterComponent extends Component { + static propTypes = { + history: PropTypes.shape({ + push: PropTypes.func.isRequired, + createHref: PropTypes.func.isRequired, + }).isRequired, + }; constructor(...args) { super(...args); @@ -30,8 +28,8 @@ class ShareRouter extends Component { registerRouter() { // Share the router with the app without requiring React or context. - const { router } = this.context; - registerRouter(router); + const { history } = this.props; + registerRouter({ history }); } render() { @@ -39,6 +37,8 @@ class ShareRouter extends Component { } } +const ShareRouter = withRouter(ShareRouterComponent); + export class App extends Component { // eslint-disable-line react/no-multi-comp componentDidMount() { trackUiMetric(METRIC_TYPE.LOADED, UIM_APP_LOAD); diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js index f7b75bcfad6f42..b1b9eb48d2e6ee 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/detail_panel/detail_panel.test.js @@ -17,6 +17,7 @@ import { tabToHumanizedMap, } from '../../components'; +jest.mock('ui/new_platform'); jest.mock('../../../services', () => { const services = require.requireActual('../../../services'); return { diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js index aabe240af0e6b3..3a565909ed68c6 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js @@ -8,6 +8,8 @@ import { registerTestBed } from '../../../../../../../test_utils'; import { rollupJobsStore } from '../../store'; import { JobList } from './job_list'; +jest.mock('ui/new_platform'); + jest.mock('ui/chrome', () => ({ addBasePath: () => {}, breadcrumbs: { set: () => {} }, diff --git a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js index a68c8053908078..2a116e7b0e1f38 100644 --- a/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js +++ b/x-pack/legacy/plugins/rollup/public/crud_app/sections/job_list/job_table/job_table.test.js @@ -11,6 +11,7 @@ import { getJobs, jobCount } from '../../../../../fixtures'; import { rollupJobsStore } from '../../../store'; import { JobTable } from './job_table'; +jest.mock('ui/new_platform'); jest.mock('../../../services', () => { const services = require.requireActual('../../../services'); return { diff --git a/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts b/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts index 7862aa386785bd..9b25f8bb36b0cf 100644 --- a/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts +++ b/x-pack/legacy/plugins/searchprofiler/server/np_ready/types.ts @@ -6,7 +6,7 @@ import { ServerRoute } from 'hapi'; import { ElasticsearchPlugin, Request } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; export type RegisterRoute = (args: ServerRoute & { config: any }) => void; diff --git a/x-pack/legacy/plugins/security/common/constants.ts b/x-pack/legacy/plugins/security/common/constants.ts deleted file mode 100644 index 08e49ad995550b..00000000000000 --- a/x-pack/legacy/plugins/security/common/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const INTERNAL_API_BASE_PATH = '/internal/security'; diff --git a/x-pack/legacy/plugins/security/common/model/index.ts b/x-pack/legacy/plugins/security/common/model.ts similarity index 84% rename from x-pack/legacy/plugins/security/common/model/index.ts rename to x-pack/legacy/plugins/security/common/model.ts index 6c2976815559ba..90e6a5403dfe8e 100644 --- a/x-pack/legacy/plugins/security/common/model/index.ts +++ b/x-pack/legacy/plugins/security/common/model.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ApiKey } from './api_key'; export { + ApiKey, + ApiKeyToInvalidate, AuthenticatedUser, BuiltinESPrivileges, EditUser, @@ -19,4 +20,4 @@ export { User, canUserChangePassword, getUserDisplayName, -} from '../../../../../plugins/security/common/model'; +} from '../../../../plugins/security/common/model'; diff --git a/x-pack/legacy/plugins/security/index.js b/x-pack/legacy/plugins/security/index.js index 1d798a4a2bc400..55963ae4b5c3db 100644 --- a/x-pack/legacy/plugins/security/index.js +++ b/x-pack/legacy/plugins/security/index.js @@ -5,10 +5,6 @@ */ import { resolve } from 'path'; -import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; -import { initUsersApi } from './server/routes/api/v1/users'; -import { initApiKeysApi } from './server/routes/api/v1/api_keys'; -import { initIndicesApi } from './server/routes/api/v1/indices'; import { initOverwrittenSessionView } from './server/routes/views/overwritten_session'; import { initLoginView } from './server/routes/views/login'; import { initLogoutView } from './server/routes/views/logout'; @@ -25,16 +21,17 @@ export const security = (kibana) => new kibana.Plugin({ require: ['kibana', 'elasticsearch', 'xpack_main'], config(Joi) { + const HANDLED_IN_NEW_PLATFORM = Joi.any().description('This key is handled in the new platform security plugin ONLY'); return Joi.object({ enabled: Joi.boolean().default(true), - cookieName: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - encryptionKey: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + cookieName: HANDLED_IN_NEW_PLATFORM, + encryptionKey: HANDLED_IN_NEW_PLATFORM, session: Joi.object({ - idleTimeout: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - lifespan: Joi.any().description('This key is handled in the new platform security plugin ONLY'), + idleTimeout: HANDLED_IN_NEW_PLATFORM, + lifespan: HANDLED_IN_NEW_PLATFORM, }).default(), - secureCookies: Joi.any().description('This key is handled in the new platform security plugin ONLY'), - loginAssistanceMessage: Joi.string().default(), + secureCookies: HANDLED_IN_NEW_PLATFORM, + loginAssistanceMessage: HANDLED_IN_NEW_PLATFORM, authorization: Joi.object({ legacyFallback: Joi.object({ enabled: Joi.boolean().default(true) // deprecated @@ -43,7 +40,7 @@ export const security = (kibana) => new kibana.Plugin({ audit: Joi.object({ enabled: Joi.boolean().default(false) }).default(), - authc: Joi.any().description('This key is handled in the new platform security plugin ONLY') + authc: HANDLED_IN_NEW_PLATFORM }).default(); }, @@ -95,8 +92,6 @@ export const security = (kibana) => new kibana.Plugin({ secureCookies: securityPlugin.__legacyCompat.config.secureCookies, session: { tenant: server.newPlatform.setup.core.http.basePath.serverBasePath, - idleTimeout: securityPlugin.__legacyCompat.config.session.idleTimeout, - lifespan: securityPlugin.__legacyCompat.config.session.lifespan, }, enableSpaceAwarePrivileges: server.config().get('xpack.spaces.enabled'), }; @@ -132,7 +127,6 @@ export const security = (kibana) => new kibana.Plugin({ server.plugins.kibana.systemApi ), cspRules: createCSPRuleString(config.get('csp.rules')), - kibanaIndexName: config.get('kibana.index'), }); // Legacy xPack Info endpoint returns whatever we return in a callback for `registerLicenseCheckResultsGenerator` @@ -145,10 +139,6 @@ export const security = (kibana) => new kibana.Plugin({ server.expose({ getUser: request => securityPlugin.authc.getCurrentUser(KibanaRequest.from(request)) }); - initAuthenticateApi(securityPlugin, server); - initUsersApi(securityPlugin, server); - initApiKeysApi(server); - initIndicesApi(server); initLoginView(securityPlugin, server); initLogoutView(server); initLoggedOutView(securityPlugin, server); diff --git a/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js b/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js index 6d03f3da6e2f26..efc227e2c2789a 100644 --- a/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js +++ b/x-pack/legacy/plugins/security/public/hacks/on_unauthorized_response.js @@ -11,8 +11,8 @@ import 'plugins/security/services/auto_logout'; function isUnauthorizedResponseAllowed(response) { const API_WHITELIST = [ - '/api/security/v1/login', - '/api/security/v1/users/.*/password' + '/internal/security/login', + '/internal/security/users/.*/password' ]; const url = response.config.url; diff --git a/x-pack/legacy/plugins/security/public/lib/api.ts b/x-pack/legacy/plugins/security/public/lib/api.ts index e6e42ed5bd4da2..ffa08ca44f3765 100644 --- a/x-pack/legacy/plugins/security/public/lib/api.ts +++ b/x-pack/legacy/plugins/security/public/lib/api.ts @@ -7,12 +7,12 @@ import { kfetch } from 'ui/kfetch'; import { AuthenticatedUser, Role, User, EditUser } from '../../common/model'; -const usersUrl = '/api/security/v1/users'; +const usersUrl = '/internal/security/users'; const rolesUrl = '/api/security/role'; export class UserAPIClient { public async getCurrentUser(): Promise { - return await kfetch({ pathname: `/api/security/v1/me` }); + return await kfetch({ pathname: `/internal/security/me` }); } public async getUsers(): Promise { diff --git a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts index c6dcef392af989..fbc0460c5908a7 100644 --- a/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts +++ b/x-pack/legacy/plugins/security/public/lib/api_keys_api.ts @@ -5,8 +5,7 @@ */ import { kfetch } from 'ui/kfetch'; -import { ApiKey, ApiKeyToInvalidate } from '../../common/model/api_key'; -import { INTERNAL_API_BASE_PATH } from '../../common/constants'; +import { ApiKey, ApiKeyToInvalidate } from '../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; @@ -22,7 +21,7 @@ interface GetApiKeysResponse { apiKeys: ApiKey[]; } -const apiKeysUrl = `${INTERNAL_API_BASE_PATH}/api_key`; +const apiKeysUrl = `/internal/security/api_key`; export class ApiKeysApi { public static async checkPrivileges(): Promise { diff --git a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts index e0998eb8b8f6be..91d98782dab423 100644 --- a/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts +++ b/x-pack/legacy/plugins/security/public/objects/lib/get_fields.ts @@ -6,7 +6,7 @@ import { IHttpResponse } from 'angular'; import chrome from 'ui/chrome'; -const apiBase = chrome.addBasePath(`/api/security/v1/fields`); +const apiBase = chrome.addBasePath(`/internal/security/fields`); export async function getFields($http: any, query: string): Promise { return await $http diff --git a/x-pack/legacy/plugins/security/public/services/shield_indices.js b/x-pack/legacy/plugins/security/public/services/shield_indices.js index 2e25d73acbcee6..973569eb6e9c3f 100644 --- a/x-pack/legacy/plugins/security/public/services/shield_indices.js +++ b/x-pack/legacy/plugins/security/public/services/shield_indices.js @@ -10,7 +10,7 @@ const module = uiModules.get('security', []); module.service('shieldIndices', ($http, chrome) => { return { getFields: (query) => { - return $http.get(chrome.addBasePath(`/api/security/v1/fields/${query}`)) + return $http.get(chrome.addBasePath(`/internal/security/fields/${query}`)) .then(response => response.data); } }; diff --git a/x-pack/legacy/plugins/security/public/services/shield_user.js b/x-pack/legacy/plugins/security/public/services/shield_user.js index e77895caaa2baf..53252e851e3536 100644 --- a/x-pack/legacy/plugins/security/public/services/shield_user.js +++ b/x-pack/legacy/plugins/security/public/services/shield_user.js @@ -10,7 +10,7 @@ import { uiModules } from 'ui/modules'; const module = uiModules.get('security', ['ngResource']); module.service('ShieldUser', ($resource, chrome) => { - const baseUrl = chrome.addBasePath('/api/security/v1/users/:username'); + const baseUrl = chrome.addBasePath('/internal/security/users/:username'); const ShieldUser = $resource(baseUrl, { username: '@username' }, { @@ -21,7 +21,7 @@ module.service('ShieldUser', ($resource, chrome) => { }, getCurrent: { method: 'GET', - url: chrome.addBasePath('/api/security/v1/me') + url: chrome.addBasePath('/internal/security/me') } }); diff --git a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx index acdc29842d4c65..e6d3b5b7536b6a 100644 --- a/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/login/components/basic_login_form/basic_login_form.tsx @@ -190,7 +190,7 @@ class BasicLoginFormUI extends Component { const { username, password } = this.state; - http.post('./api/security/v1/login', { username, password }).then( + http.post('./internal/security/login', { username, password }).then( () => (window.location.href = next), (error: any) => { const { statusCode = 500 } = error.data || {}; diff --git a/x-pack/legacy/plugins/security/public/views/logout/logout.js b/x-pack/legacy/plugins/security/public/views/logout/logout.js index 4411ecdade8e75..5d76dfc2908c8f 100644 --- a/x-pack/legacy/plugins/security/public/views/logout/logout.js +++ b/x-pack/legacy/plugins/security/public/views/logout/logout.js @@ -12,5 +12,5 @@ chrome $window.sessionStorage.clear(); // Redirect user to the server logout endpoint to complete logout. - $window.location.href = chrome.addBasePath(`/api/security/v1/logout${$window.location.search}`); + $window.location.href = chrome.addBasePath(`/api/security/logout${$window.location.search}`); }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx index 37838cfdb950d9..1613e3804c31dd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx @@ -29,7 +29,7 @@ import _ from 'lodash'; import { toastNotifications } from 'ui/notify'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model/api_key'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; import { ApiKeysApi } from '../../../../lib/api_keys_api'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx index fe9ffc651db29c..a1627442b89b81 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/invalidate_provider/invalidate_provider.tsx @@ -8,7 +8,7 @@ import React, { Fragment, useRef, useState } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { ApiKeyToInvalidate } from '../../../../../../common/model/api_key'; +import { ApiKeyToInvalidate } from '../../../../../../common/model'; import { ApiKeysApi } from '../../../../../lib/api_keys_api'; interface Props { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx index cb60b773f92e0e..67c32c8393171f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx @@ -9,8 +9,11 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { UICapabilities } from 'ui/capabilities'; import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../plugins/features/public'; +// These modules should be moved into a common directory +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; import { RawKibanaPrivileges, Role } from '../../../../../common/model'; import { EditRolePage } from './edit_role_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx index c5bf910b007d00..7637d28dd42297 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx @@ -23,7 +23,7 @@ import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; import { UICapabilities } from 'ui/capabilities'; import { toastNotifications } from 'ui/notify'; import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../plugins/features/public'; import { KibanaPrivileges, RawKibanaPrivileges, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx index 8425826235f0bf..a05dc687fce4ac 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx @@ -17,7 +17,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { AllowedPrivilege, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.tsx index 199067b2e74697..97d61916926b69 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.tsx @@ -8,7 +8,7 @@ import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; import { UICapabilities } from 'ui/capabilities'; import { Space } from '../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role } from '../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../lib/kibana_privilege_calculator'; import { RoleValidator } from '../../../lib/validate_role'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index 74d62b0c867580..1f29f774fd6cc0 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -7,7 +7,7 @@ import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; import { SimplePrivilegeSection } from './simple_privilege_section'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index d564179798ad87..7768dc769a32f7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -15,7 +15,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index 3d4a0d89ed7a1f..ee121caa13a2af 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -8,7 +8,7 @@ import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator'; import { PrivilegeMatrix } from './privilege_matrix'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index be49494efbe9a8..92dace65d466c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -24,7 +24,7 @@ import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; import { SpaceAvatar } from '../../../../../../../../../spaces/public/components'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index a616d3537cee30..5abb87d23bb6e6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -25,7 +25,7 @@ import { import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { AllowedPrivilege, diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index cdb5521bd0c863..d324cf99c8418a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -16,7 +16,7 @@ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { UICapabilities } from 'ui/capabilities'; import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../../../../plugins/features/public'; import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; import { isReservedRole } from '../../../../../../../lib/role_utils'; diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts deleted file mode 100644 index c928a38d88ef3a..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/request.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Request } from 'hapi'; -import url from 'url'; - -interface RequestFixtureOptions { - headers?: Record; - auth?: string; - params?: Record; - path?: string; - basePath?: string; - search?: string; - payload?: unknown; -} - -export function requestFixture({ - headers = { accept: 'something/html' }, - auth, - params, - path = '/wat', - search = '', - payload, -}: RequestFixtureOptions = {}) { - return ({ - raw: { req: { headers } }, - auth, - headers, - params, - url: { path, search }, - query: search ? url.parse(search, true /* parseQueryString */).query : {}, - payload, - state: { user: 'these are the contents of the user client cookie' }, - route: { settings: {} }, - } as any) as Request; -} diff --git a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts b/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts deleted file mode 100644 index 55b6f735cfced8..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/__tests__/__fixtures__/server.ts +++ /dev/null @@ -1,56 +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 { stub } from 'sinon'; - -export function serverFixture() { - return { - config: stub(), - register: stub(), - expose: stub(), - log: stub(), - route: stub(), - decorate: stub(), - - info: { - protocol: 'protocol', - }, - - auth: { - strategy: stub(), - test: stub(), - }, - - plugins: { - elasticsearch: { - createCluster: stub(), - }, - - kibana: { - systemApi: { isSystemApiRequest: stub() }, - }, - - security: { - getUser: stub(), - authenticate: stub(), - deauthenticate: stub(), - authorization: { - application: stub(), - }, - }, - - xpack_main: { - info: { - isAvailable: stub(), - feature: stub(), - license: { - isOneOf: stub(), - }, - }, - }, - }, - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js b/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js deleted file mode 100644 index 64816bf4d23d70..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/route_pre_check_license.js +++ /dev/null @@ -1,18 +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. - */ - -const Boom = require('boom'); - -export function routePreCheckLicense(server) { - return function forbidApiAccess() { - const licenseCheckResults = server.newPlatform.setup.plugins.security.__legacyCompat.license.getFeatures(); - if (!licenseCheckResults.showLinks) { - throw Boom.forbidden(licenseCheckResults.linksMessage); - } else { - return null; - } - }; -} diff --git a/x-pack/legacy/plugins/security/server/lib/user_schema.js b/x-pack/legacy/plugins/security/server/lib/user_schema.js deleted file mode 100644 index 57c66b2712025b..00000000000000 --- a/x-pack/legacy/plugins/security/server/lib/user_schema.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Joi from 'joi'; - -export const userSchema = Joi.object({ - username: Joi.string().required(), - password: Joi.string(), - roles: Joi.array().items(Joi.string()), - full_name: Joi.string().allow(null, ''), - email: Joi.string().allow(null, ''), - metadata: Joi.object(), - enabled: Joi.boolean().default(true) -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js deleted file mode 100644 index 5cea7c70b77811..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/authenticate.js +++ /dev/null @@ -1,260 +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 expect from '@kbn/expect'; -import Boom from 'boom'; -import Joi from 'joi'; -import sinon from 'sinon'; - -import { serverFixture } from '../../../../lib/__tests__/__fixtures__/server'; -import { requestFixture } from '../../../../lib/__tests__/__fixtures__/request'; -import { AuthenticationResult, DeauthenticationResult } from '../../../../../../../../plugins/security/server'; -import { initAuthenticateApi } from '../authenticate'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; - -describe('Authentication routes', () => { - let serverStub; - let hStub; - let loginStub; - let logoutStub; - - beforeEach(() => { - serverStub = serverFixture(); - hStub = { - authenticated: sinon.stub(), - continue: 'blah', - redirect: sinon.stub(), - response: sinon.stub() - }; - loginStub = sinon.stub(); - logoutStub = sinon.stub(); - - initAuthenticateApi({ - authc: { login: loginStub, logout: logoutStub }, - __legacyCompat: { config: { authc: { providers: ['basic'] } } }, - }, serverStub); - }); - - describe('login', () => { - let loginRoute; - let request; - - beforeEach(() => { - loginRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/login' })) - .firstCall - .args[0]; - - request = requestFixture({ - headers: {}, - payload: { username: 'user', password: 'password' } - }); - }); - - it('correctly defines route.', async () => { - expect(loginRoute.method).to.be('POST'); - expect(loginRoute.path).to.be('/api/security/v1/login'); - expect(loginRoute.handler).to.be.a(Function); - expect(loginRoute.config).to.eql({ - auth: false, - validate: { - payload: Joi.object({ - username: Joi.string().required(), - password: Joi.string().required() - }) - }, - response: { - emptyStatusCode: 204, - } - }); - }); - - it('returns 500 if authentication throws unhandled exception.', async () => { - const unhandledException = new Error('Something went wrong.'); - loginStub.throws(unhandledException); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - }); - - it('returns 401 if authentication fails.', async () => { - const failureReason = new Error('Something went wrong.'); - loginStub.resolves(AuthenticationResult.failed(failureReason)); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be(failureReason.message); - expect(response.output.statusCode).to.be(401); - }); - }); - - it('returns 401 if authentication is not handled.', async () => { - loginStub.resolves(AuthenticationResult.notHandled()); - - return loginRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Unauthorized'); - expect(response.output.statusCode).to.be(401); - }); - }); - - describe('authentication succeeds', () => { - - it(`returns user data`, async () => { - loginStub.resolves(AuthenticationResult.succeeded({ username: 'user' })); - - await loginRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.response); - sinon.assert.calledOnce(loginStub); - sinon.assert.calledWithExactly( - loginStub, - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'password' } } - ); - }); - }); - - }); - - describe('logout', () => { - let logoutRoute; - - beforeEach(() => { - serverStub.config.returns({ - get: sinon.stub().withArgs('server.basePath').returns('/test-base-path') - }); - - logoutRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/logout' })) - .firstCall - .args[0]; - }); - - it('correctly defines route.', async () => { - expect(logoutRoute.method).to.be('GET'); - expect(logoutRoute.path).to.be('/api/security/v1/logout'); - expect(logoutRoute.handler).to.be.a(Function); - expect(logoutRoute.config).to.eql({ auth: false }); - }); - - it('returns 500 if deauthentication throws unhandled exception.', async () => { - const request = requestFixture(); - - const unhandledException = new Error('Something went wrong.'); - logoutStub.rejects(unhandledException); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response).to.be(Boom.boomify(unhandledException)); - sinon.assert.notCalled(hStub.redirect); - }); - }); - - it('returns 500 if authenticator fails to logout.', async () => { - const request = requestFixture(); - - const failureReason = Boom.forbidden(); - logoutStub.resolves(DeauthenticationResult.failed(failureReason)); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response).to.be(Boom.boomify(failureReason)); - sinon.assert.notCalled(hStub.redirect); - sinon.assert.calledOnce(logoutStub); - sinon.assert.calledWithExactly( - logoutStub, - sinon.match.instanceOf(KibanaRequest) - ); - }); - }); - - it('returns 400 for AJAX requests that can not handle redirect.', async () => { - const request = requestFixture({ headers: { 'kbn-xsrf': 'xsrf' } }); - - return logoutRoute - .handler(request, hStub) - .catch((response) => { - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Client should be able to process redirect response.'); - expect(response.output.statusCode).to.be(400); - sinon.assert.notCalled(hStub.redirect); - }); - }); - - it('redirects user to the URL returned by authenticator.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.redirectTo('https://custom.logout')); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, 'https://custom.logout'); - }); - - it('redirects user to the base path if deauthentication succeeds.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.succeeded()); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, '/test-base-path/'); - }); - - it('redirects user to the base path if deauthentication is not handled.', async () => { - const request = requestFixture(); - - logoutStub.resolves(DeauthenticationResult.notHandled()); - - await logoutRoute.handler(request, hStub); - - sinon.assert.calledOnce(hStub.redirect); - sinon.assert.calledWithExactly(hStub.redirect, '/test-base-path/'); - }); - }); - - describe('me', () => { - let meRoute; - - beforeEach(() => { - meRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/me' })) - .firstCall - .args[0]; - }); - - it('correctly defines route.', async () => { - expect(meRoute.method).to.be('GET'); - expect(meRoute.path).to.be('/api/security/v1/me'); - expect(meRoute.handler).to.be.a(Function); - expect(meRoute.config).to.be(undefined); - }); - - it('returns user from the authenticated request property.', async () => { - const request = { auth: { credentials: { username: 'user' } } }; - const response = await meRoute.handler(request, hStub); - - expect(response).to.eql({ username: 'user' }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js b/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js deleted file mode 100644 index 4077ab52e86de7..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/__tests__/users.js +++ /dev/null @@ -1,214 +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 expect from '@kbn/expect'; -import Joi from 'joi'; -import sinon from 'sinon'; - -import { serverFixture } from '../../../../lib/__tests__/__fixtures__/server'; -import { requestFixture } from '../../../../lib/__tests__/__fixtures__/request'; -import { AuthenticationResult } from '../../../../../../../../plugins/security/server'; -import { initUsersApi } from '../users'; -import * as ClientShield from '../../../../../../../server/lib/get_client_shield'; -import { KibanaRequest } from '../../../../../../../../../src/core/server'; - -describe('User routes', () => { - const sandbox = sinon.createSandbox(); - - let clusterStub; - let serverStub; - let loginStub; - - beforeEach(() => { - serverStub = serverFixture(); - loginStub = sinon.stub(); - - // Cluster is returned by `getClient` function that is wrapped into `once` making cluster - // a static singleton, so we should use sandbox to set/reset its behavior between tests. - clusterStub = sinon.stub({ callWithRequest() {} }); - sandbox.stub(ClientShield, 'getClient').returns(clusterStub); - - initUsersApi({ authc: { login: loginStub }, __legacyCompat: { config: { authc: { providers: ['basic'] } } } }, serverStub); - }); - - afterEach(() => sandbox.restore()); - - describe('change password', () => { - let changePasswordRoute; - let request; - - beforeEach(() => { - changePasswordRoute = serverStub.route - .withArgs(sinon.match({ path: '/api/security/v1/users/{username}/password' })) - .firstCall - .args[0]; - - request = requestFixture({ - headers: {}, - auth: { credentials: { username: 'user' } }, - params: { username: 'target-user' }, - payload: { password: 'old-password', newPassword: 'new-password' } - }); - }); - - it('correctly defines route.', async () => { - expect(changePasswordRoute.method).to.be('POST'); - expect(changePasswordRoute.path).to.be('/api/security/v1/users/{username}/password'); - expect(changePasswordRoute.handler).to.be.a(Function); - - expect(changePasswordRoute.config).to.not.have.property('auth'); - expect(changePasswordRoute.config).to.have.property('pre'); - expect(changePasswordRoute.config.pre).to.have.length(1); - expect(changePasswordRoute.config.validate).to.eql({ - payload: Joi.object({ - password: Joi.string(), - newPassword: Joi.string().required() - }) - }); - }); - - describe('own password', () => { - beforeEach(() => { - request.params.username = request.auth.credentials.username; - loginStub = loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'old-password' }, stateless: true } - ) - .resolves(AuthenticationResult.succeeded({})); - }); - - it('returns 403 if old password is wrong.', async () => { - loginStub.resolves(AuthenticationResult.failed(new Error('Something went wrong.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.notCalled(clusterStub.callWithRequest); - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: 'Something went wrong.' - }); - }); - - it(`returns 401 if user can't authenticate with new password.`, async () => { - loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'new-password' } } - ) - .resolves(AuthenticationResult.failed(new Error('Something went wrong.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 401, - error: 'Unauthorized', - message: 'Something went wrong.' - }); - }); - - it('returns 500 if password update request fails.', async () => { - clusterStub.callWithRequest - .withArgs( - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ) - .rejects(new Error('Request failed.')); - - const response = await changePasswordRoute.handler(request); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - - it('successfully changes own password if provided old password is correct.', async () => { - loginStub - .withArgs( - sinon.match.instanceOf(KibanaRequest), - { provider: 'basic', value: { username: 'user', password: 'new-password' } } - ) - .resolves(AuthenticationResult.succeeded({})); - - const hResponseStub = { code: sinon.stub() }; - const hStub = { response: sinon.stub().returns(hResponseStub) }; - - await changePasswordRoute.handler(request, hStub); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'user', body: { password: 'new-password' } } - ); - - sinon.assert.calledWithExactly(hStub.response); - sinon.assert.calledWithExactly(hResponseStub.code, 204); - }); - }); - - describe('other user password', () => { - it('returns 500 if password update request fails.', async () => { - clusterStub.callWithRequest - .withArgs( - sinon.match.same(request), - 'shield.changePassword', - { username: 'target-user', body: { password: 'new-password' } } - ) - .returns(Promise.reject(new Error('Request failed.'))); - - const response = await changePasswordRoute.handler(request); - - sinon.assert.notCalled(serverStub.plugins.security.getUser); - sinon.assert.notCalled(loginStub); - - expect(response.isBoom).to.be(true); - expect(response.output.payload).to.eql({ - statusCode: 500, - error: 'Internal Server Error', - message: 'An internal server error occurred' - }); - }); - - it('successfully changes user password.', async () => { - const hResponseStub = { code: sinon.stub() }; - const hStub = { response: sinon.stub().returns(hResponseStub) }; - - await changePasswordRoute.handler(request, hStub); - - sinon.assert.notCalled(serverStub.plugins.security.getUser); - sinon.assert.notCalled(loginStub); - - sinon.assert.calledOnce(clusterStub.callWithRequest); - sinon.assert.calledWithExactly( - clusterStub.callWithRequest, - sinon.match.same(request), - 'shield.changePassword', - { username: 'target-user', body: { password: 'new-password' } } - ); - - sinon.assert.calledWithExactly(hStub.response); - sinon.assert.calledWithExactly(hResponseStub.code, 204); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js deleted file mode 100644 index a236badcd0d6bb..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.js +++ /dev/null @@ -1,45 +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 Joi from 'joi'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'GET', - path: `${INTERNAL_API_BASE_PATH}/api_key`, - async handler(request) { - try { - const { isAdmin } = request.query; - - const result = await callWithRequest( - request, - 'shield.getAPIKeys', - { - owner: !isAdmin - } - ); - - const validKeys = result.api_keys.filter(({ invalidated }) => !invalidated); - - return { - apiKeys: validKeys, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn], - validate: { - query: Joi.object().keys({ - isAdmin: Joi.bool().required(), - }).required(), - }, - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js deleted file mode 100644 index 400e5b705aeb2a..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/get.test.js +++ /dev/null @@ -1,166 +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 Hapi from 'hapi'; -import Boom from 'boom'; - -import { initGetApiKeysApi } from './get'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('GET API keys', () => { - const getApiKeysTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpl, - asserts, - isAdmin = true, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - if (callWithRequestImpl) { - mockCallWithRequest.mockImplementation(callWithRequestImpl); - } - - initGetApiKeysApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: `${INTERNAL_API_BASE_PATH}/api_key?isAdmin=${isAdmin}`, - headers, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (callWithRequestImpl) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - 'shield.getAPIKeys', - { - owner: !isAdmin, - }, - ); - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getApiKeysTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getApiKeysTest('returns error from callWithRequest', { - callWithRequestImpl: async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, - asserts: { - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - }); - - describe('success', () => { - getApiKeysTest('returns API keys', { - callWithRequestImpl: async () => ({ - api_keys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }), - asserts: { - statusCode: 200, - result: { - apiKeys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }, - }, - }); - getApiKeysTest('returns only valid API keys', { - callWithRequestImpl: async () => ({ - api_keys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key1', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: true, - username: 'elastic', - realm: 'reserved' - }, { - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key2', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }], - }), - asserts: { - statusCode: 200, - result: { - apiKeys: - [{ - id: 'YCLV7m0BJ3xI4hhWB648', - name: 'test-api-key2', - creation: 1571670001452, - expiration: 1571756401452, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js deleted file mode 100644 index fc55bdcc386616..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/index.js +++ /dev/null @@ -1,20 +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 { getClient } from '../../../../../../../server/lib/get_client_shield'; -import { routePreCheckLicense } from '../../../../lib/route_pre_check_license'; -import { initCheckPrivilegesApi } from './privileges'; -import { initGetApiKeysApi } from './get'; -import { initInvalidateApiKeysApi } from './invalidate'; - -export function initApiKeysApi(server) { - const callWithRequest = getClient(server).callWithRequest; - const routePreCheckLicenseFn = routePreCheckLicense(server); - - initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn); - initGetApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); - initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js deleted file mode 100644 index 293142c60be674..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.js +++ /dev/null @@ -1,70 +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 Joi from 'joi'; -import { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initInvalidateApiKeysApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'POST', - path: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`, - async handler(request) { - try { - const { apiKeys, isAdmin } = request.payload; - const itemsInvalidated = []; - const errors = []; - - // Send the request to invalidate the API key and return an error if it could not be deleted. - const sendRequestToInvalidateApiKey = async (id) => { - try { - const body = { id }; - - if (!isAdmin) { - body.owner = true; - } - - await callWithRequest(request, 'shield.invalidateAPIKey', { body }); - return null; - } catch (error) { - return wrapError(error); - } - }; - - const invalidateApiKey = async ({ id, name }) => { - const error = await sendRequestToInvalidateApiKey(id); - if (error) { - errors.push({ id, name, error }); - } else { - itemsInvalidated.push({ id, name }); - } - }; - - // Invalidate all API keys in parallel. - await Promise.all(apiKeys.map((key) => invalidateApiKey(key))); - - return { - itemsInvalidated, - errors, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn], - validate: { - payload: Joi.object({ - apiKeys: Joi.array().items(Joi.object({ - id: Joi.string().required(), - name: Joi.string().required(), - })).required(), - isAdmin: Joi.bool().required(), - }) - }, - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js deleted file mode 100644 index 3ed7ca94eb782a..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/invalidate.test.js +++ /dev/null @@ -1,200 +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 Hapi from 'hapi'; -import Boom from 'boom'; - -import { initInvalidateApiKeysApi } from './invalidate'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('POST invalidate', () => { - const postInvalidateTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpls = [], - asserts, - payload, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - for (const impl of callWithRequestImpls) { - mockCallWithRequest.mockImplementationOnce(impl); - } - - initInvalidateApiKeysApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'POST', - url: `${INTERNAL_API_BASE_PATH}/api_key/invalidate`, - headers, - payload, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (asserts.callWithRequests) { - for (const args of asserts.callWithRequests) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - ...args - ); - } - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - postInvalidateTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], - isAdmin: true - }, - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - postInvalidateTest('returns errors array from callWithRequest', { - callWithRequestImpls: [async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [], - errors: [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - error: Boom.notAcceptable('test not acceptable message'), - }] - }, - }, - }); - }); - - describe('success', () => { - postInvalidateTest('invalidates API keys', { - callWithRequestImpls: [async () => null], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - errors: [], - }, - }, - }); - - postInvalidateTest('adds "owner" to body if isAdmin=false', { - callWithRequestImpls: [async () => null], - payload: { - apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key', }], - isAdmin: false - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - owner: true, - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], - errors: [], - }, - }, - }); - - postInvalidateTest('returns only successful invalidation requests', { - callWithRequestImpls: [ - async () => null, - async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - payload: { - apiKeys: [ - { id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }, - { id: 'ab8If24B1bKsmSLTAhNC', name: 'my-api-key2' } - ], - isAdmin: true - }, - asserts: { - callWithRequests: [ - ['shield.invalidateAPIKey', { - body: { - id: 'si8If24B1bKsmSLTAhJV', - }, - }], - ['shield.invalidateAPIKey', { - body: { - id: 'ab8If24B1bKsmSLTAhNC', - }, - }], - ], - statusCode: 200, - result: { - itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }], - errors: [{ - id: 'ab8If24B1bKsmSLTAhNC', - name: 'my-api-key2', - error: Boom.notAcceptable('test not acceptable message'), - }] - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js deleted file mode 100644 index 3aa30c9a3b9bb3..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.js +++ /dev/null @@ -1,75 +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 { wrapError } from '../../../../../../../../plugins/security/server'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -export function initCheckPrivilegesApi(server, callWithRequest, routePreCheckLicenseFn) { - server.route({ - method: 'GET', - path: `${INTERNAL_API_BASE_PATH}/api_key/privileges`, - async handler(request) { - try { - const result = await Promise.all([ - callWithRequest( - request, - 'shield.hasPrivileges', - { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - } - ), - new Promise(async (resolve, reject) => { - try { - const result = await callWithRequest( - request, - 'shield.getAPIKeys', - { - owner: true - } - ); - // If the API returns a truthy result that means it's enabled. - resolve({ areApiKeysEnabled: !!result }); - } catch (e) { - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - if (e.message.includes('api keys are not enabled')) { - return resolve({ areApiKeysEnabled: false }); - } - - // It's a real error, so rethrow it. - reject(e); - } - }), - ]); - - const [{ - cluster: { - manage_security: manageSecurity, - manage_api_key: manageApiKey, - } - }, { - areApiKeysEnabled, - }] = result; - - const isAdmin = manageSecurity || manageApiKey; - - return { - areApiKeysEnabled, - isAdmin, - }; - } catch (error) { - return wrapError(error); - } - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js b/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js deleted file mode 100644 index 2a6f935e005950..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/api_keys/privileges.test.js +++ /dev/null @@ -1,254 +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 Hapi from 'hapi'; -import Boom from 'boom'; - -import { initCheckPrivilegesApi } from './privileges'; -import { INTERNAL_API_BASE_PATH } from '../../../../../common/constants'; - -const createMockServer = () => new Hapi.Server({ debug: false, port: 8080 }); - -describe('GET privileges', () => { - const getPrivilegesTest = ( - description, - { - preCheckLicenseImpl = () => null, - callWithRequestImpls = [], - asserts, - } - ) => { - test(description, async () => { - const mockServer = createMockServer(); - const pre = jest.fn().mockImplementation(preCheckLicenseImpl); - const mockCallWithRequest = jest.fn(); - - for (const impl of callWithRequestImpls) { - mockCallWithRequest.mockImplementationOnce(impl); - } - - initCheckPrivilegesApi(mockServer, mockCallWithRequest, pre); - - const headers = { - authorization: 'foo', - }; - - const request = { - method: 'GET', - url: `${INTERNAL_API_BASE_PATH}/api_key/privileges`, - headers, - }; - - const { result, statusCode } = await mockServer.inject(request); - - expect(pre).toHaveBeenCalled(); - - if (asserts.callWithRequests) { - for (const args of asserts.callWithRequests) { - expect(mockCallWithRequest).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }), - ...args - ); - } - } else { - expect(mockCallWithRequest).not.toHaveBeenCalled(); - } - - expect(statusCode).toBe(asserts.statusCode); - expect(result).toEqual(asserts.result); - }); - }; - - describe('failure', () => { - getPrivilegesTest('returns result of routePreCheckLicense', { - preCheckLicenseImpl: () => Boom.forbidden('test forbidden message'), - asserts: { - statusCode: 403, - result: { - error: 'Forbidden', - statusCode: 403, - message: 'test forbidden message', - }, - }, - }); - - getPrivilegesTest('returns error from first callWithRequest', { - callWithRequestImpls: [async () => { - throw Boom.notAcceptable('test not acceptable message'); - }, async () => { }], - asserts: { - callWithRequests: [ - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ['shield.getAPIKeys', { owner: true }], - ], - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - - getPrivilegesTest('returns error from second callWithRequest', { - callWithRequestImpls: [async () => { }, async () => { - throw Boom.notAcceptable('test not acceptable message'); - }], - asserts: { - callWithRequests: [ - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ['shield.getAPIKeys', { owner: true }], - ], - statusCode: 406, - result: { - error: 'Not Acceptable', - statusCode: 406, - message: 'test not acceptable message', - }, - }, - }); - }); - - describe('success', () => { - getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, - index: {}, - application: {} - }), - async () => ( - { - api_keys: - [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - } - ), - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: true, - isAdmin: true, - }, - }, - }); - - getPrivilegesTest('returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, - index: {}, - application: {} - }), - async () => { - throw Boom.unauthorized('api keys are not enabled'); - }, - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: false, - isAdmin: true, - }, - }, - }); - - getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - callWithRequestImpls: [ - async () => ({ - username: 'elastic', - has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, - index: {}, - application: {} - }), - async () => ( - { - api_keys: - [{ - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved' - }] - } - ), - ], - asserts: { - callWithRequests: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { - body: { - cluster: [ - 'manage_security', - 'manage_api_key', - ], - }, - }], - ], - statusCode: 200, - result: { - areApiKeysEnabled: true, - isAdmin: false, - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js b/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js deleted file mode 100644 index f37c9a2fd917f1..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/authenticate.js +++ /dev/null @@ -1,227 +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 Boom from 'boom'; -import Joi from 'joi'; -import { schema } from '@kbn/config-schema'; -import { canRedirectRequest, wrapError, OIDCAuthenticationFlow } from '../../../../../../../plugins/security/server'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { createCSPRuleString } from '../../../../../../../../src/legacy/server/csp'; - -export function initAuthenticateApi({ authc: { login, logout }, __legacyCompat: { config } }, server) { - function prepareCustomResourceResponse(response, contentType) { - return response - .header('cache-control', 'private, no-cache, no-store') - .header('content-security-policy', createCSPRuleString(server.config().get('csp.rules'))) - .type(contentType); - } - - server.route({ - method: 'POST', - path: '/api/security/v1/login', - config: { - auth: false, - validate: { - payload: Joi.object({ - username: Joi.string().required(), - password: Joi.string().required() - }) - }, - response: { - emptyStatusCode: 204, - } - }, - async handler(request, h) { - const { username, password } = request.payload; - - try { - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') - ? 'token' - : 'basic'; - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password } - }); - - if (!authenticationResult.succeeded()) { - throw Boom.unauthorized(authenticationResult.error); - } - - return h.response(); - } catch(err) { - throw wrapError(err); - } - } - }); - - /** - * The route should be configured as a redirect URI in OP when OpenID Connect implicit flow - * is used, so that we can extract authentication response from URL fragment and send it to - * the `/api/security/v1/oidc` route. - */ - server.route({ - method: 'GET', - path: '/api/security/v1/oidc/implicit', - config: { auth: false }, - async handler(request, h) { - return prepareCustomResourceResponse( - h.response(` - - Kibana OpenID Connect Login - - `), - 'text/html' - ); - } - }); - - /** - * The route that accompanies `/api/security/v1/oidc/implicit` and renders a JavaScript snippet - * that extracts fragment part from the URL and send it to the `/api/security/v1/oidc` route. - * We need this separate endpoint because of default CSP policy that forbids inline scripts. - */ - server.route({ - method: 'GET', - path: '/api/security/v1/oidc/implicit.js', - config: { auth: false }, - async handler(request, h) { - return prepareCustomResourceResponse( - h.response(` - window.location.replace( - '${server.config().get('server.basePath')}/api/security/v1/oidc?authenticationResponseURI=' + - encodeURIComponent(window.location.href) - ); - `), - 'text/javascript' - ); - } - }); - - server.route({ - // POST is only allowed for Third Party initiated authentication - // Consider splitting this route into two (GET and POST) when it's migrated to New Platform. - method: ['GET', 'POST'], - path: '/api/security/v1/oidc', - config: { - auth: false, - validate: { - query: Joi.object().keys({ - iss: Joi.string().uri({ scheme: 'https' }), - login_hint: Joi.string(), - target_link_uri: Joi.string().uri(), - code: Joi.string(), - error: Joi.string(), - error_description: Joi.string(), - error_uri: Joi.string().uri(), - state: Joi.string(), - authenticationResponseURI: Joi.string(), - }).unknown(), - } - }, - async handler(request, h) { - try { - const query = request.query || {}; - const payload = request.payload || {}; - - // An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID - // Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL - // fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details - // at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth - let loginAttempt; - if (query.authenticationResponseURI) { - loginAttempt = { - flow: OIDCAuthenticationFlow.Implicit, - authenticationResponseURI: query.authenticationResponseURI, - }; - } else if (query.code || query.error) { - // An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or - // failed) authentication from an OpenID Connect Provider during authorization code authentication flow. - // See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. - loginAttempt = { - flow: OIDCAuthenticationFlow.AuthorizationCode, - // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. - authenticationResponseURI: request.url.path, - }; - } else if (query.iss || payload.iss) { - // An HTTP GET request with a query parameter named `iss` or an HTTP POST request with the same parameter in the - // payload as part of a 3rd party initiated authentication. See more details at - // https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin - loginAttempt = { - flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, - iss: query.iss || payload.iss, - loginHint: query.login_hint || payload.login_hint, - }; - } - - if (!loginAttempt) { - throw Boom.badRequest('Unrecognized login attempt.'); - } - - // We handle the fact that the user might get redirected to Kibana while already having an session - // Return an error notifying the user they are already logged in. - const authenticationResult = await login(KibanaRequest.from(request), { - provider: 'oidc', - value: loginAttempt - }); - if (authenticationResult.succeeded()) { - return Boom.forbidden( - 'Sorry, you already have an active Kibana session. ' + - 'If you want to start a new one, please logout from the existing session first.' - ); - } - - if (authenticationResult.redirected()) { - return h.redirect(authenticationResult.redirectURL); - } - - throw Boom.unauthorized(authenticationResult.error); - } catch (err) { - throw wrapError(err); - } - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/logout', - config: { - auth: false - }, - async handler(request, h) { - if (!canRedirectRequest(KibanaRequest.from(request))) { - throw Boom.badRequest('Client should be able to process redirect response.'); - } - - try { - const deauthenticationResult = await logout( - // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any - // set of query string parameters (e.g. SAML/OIDC logout request parameters). - KibanaRequest.from(request, { - query: schema.object({}, { allowUnknowns: true }), - }) - ); - if (deauthenticationResult.failed()) { - throw wrapError(deauthenticationResult.error); - } - - return h.redirect( - deauthenticationResult.redirectURL || `${server.config().get('server.basePath')}/` - ); - } catch (err) { - throw wrapError(err); - } - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/me', - handler(request) { - return request.auth.credentials; - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js b/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js deleted file mode 100644 index 7265b83783fdd2..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/indices.js +++ /dev/null @@ -1,36 +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 _ from 'lodash'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; -import { wrapError } from '../../../../../../../plugins/security/server'; - -export function initIndicesApi(server) { - const callWithRequest = getClient(server).callWithRequest; - - server.route({ - method: 'GET', - path: '/api/security/v1/fields/{query}', - handler(request) { - return callWithRequest(request, 'indices.getFieldMapping', { - index: request.params.query, - fields: '*', - allowNoIndices: false, - includeDefaults: true - }) - .then((mappings) => - _(mappings) - .map('mappings') - .flatten() - .map(_.keys) - .flatten() - .uniq() - .value() - ) - .catch(wrapError); - } - }); -} diff --git a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js b/x-pack/legacy/plugins/security/server/routes/api/v1/users.js deleted file mode 100644 index d6dc39da657b12..00000000000000 --- a/x-pack/legacy/plugins/security/server/routes/api/v1/users.js +++ /dev/null @@ -1,142 +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 _ from 'lodash'; -import Boom from 'boom'; -import Joi from 'joi'; -import { getClient } from '../../../../../../server/lib/get_client_shield'; -import { userSchema } from '../../../lib/user_schema'; -import { routePreCheckLicense } from '../../../lib/route_pre_check_license'; -import { wrapError } from '../../../../../../../plugins/security/server'; -import { KibanaRequest } from '../../../../../../../../src/core/server'; - -export function initUsersApi({ authc: { login }, __legacyCompat: { config } }, server) { - const callWithRequest = getClient(server).callWithRequest; - const routePreCheckLicenseFn = routePreCheckLicense(server); - - server.route({ - method: 'GET', - path: '/api/security/v1/users', - handler(request) { - return callWithRequest(request, 'shield.getUser').then( - _.values, - wrapError - ); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'GET', - path: '/api/security/v1/users/{username}', - handler(request) { - const username = request.params.username; - return callWithRequest(request, 'shield.getUser', { username }).then( - (response) => { - if (response[username]) return response[username]; - throw Boom.notFound(); - }, - wrapError); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/security/v1/users/{username}', - handler(request) { - const username = request.params.username; - const body = _(request.payload).omit(['username', 'enabled']).omit(_.isNull); - return callWithRequest(request, 'shield.putUser', { username, body }).then( - () => request.payload, - wrapError); - }, - config: { - validate: { - payload: userSchema - }, - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'DELETE', - path: '/api/security/v1/users/{username}', - handler(request, h) { - const username = request.params.username; - return callWithRequest(request, 'shield.deleteUser', { username }).then( - () => h.response().code(204), - wrapError); - }, - config: { - pre: [routePreCheckLicenseFn] - } - }); - - server.route({ - method: 'POST', - path: '/api/security/v1/users/{username}/password', - async handler(request, h) { - const username = request.params.username; - const { password, newPassword } = request.payload; - const isCurrentUser = username === request.auth.credentials.username; - - // We should prefer `token` over `basic` if possible. - const providerToLoginWith = config.authc.providers.includes('token') - ? 'token' - : 'basic'; - - // If user tries to change own password, let's check if old password is valid first by trying - // to login. - if (isCurrentUser) { - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password }, - // We shouldn't alter authentication state just yet. - stateless: true, - }); - - if (!authenticationResult.succeeded()) { - return Boom.forbidden(authenticationResult.error); - } - } - - try { - const body = { password: newPassword }; - await callWithRequest(request, 'shield.changePassword', { username, body }); - - // Now we authenticate user with the new password again updating current session if any. - if (isCurrentUser) { - const authenticationResult = await login(KibanaRequest.from(request), { - provider: providerToLoginWith, - value: { username, password: newPassword } - }); - - if (!authenticationResult.succeeded()) { - return Boom.unauthorized((authenticationResult.error)); - } - } - } catch(err) { - return wrapError(err); - } - - return h.response().code(204); - }, - config: { - validate: { - payload: Joi.object({ - password: Joi.string(), - newPassword: Joi.string().required() - }) - }, - pre: [routePreCheckLicenseFn] - } - }); -} diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts index 0924b6c6eb5e6a..7b5015c34de14a 100644 --- a/x-pack/legacy/plugins/siem/common/constants.ts +++ b/x-pack/legacy/plugins/siem/common/constants.ts @@ -40,12 +40,21 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; */ export const SIGNALS_ID = `${APP_ID}.signals`; +/** + * Special internal structure for tags for signals. This is used + * to filter out tags that have internal structures within them. + */ +export const INTERNAL_IDENTIFIER = '__internal'; +export const INTERNAL_RULE_ID_KEY = `${INTERNAL_IDENTIFIER}_rule_id`; + /** * Detection engine routes */ export const DETECTION_ENGINE_URL = '/api/detection_engine'; export const DETECTION_ENGINE_RULES_URL = `${DETECTION_ENGINE_URL}/rules`; +export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges`; export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; +export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags`; /** * Default signals index key for kibana.dev.yml @@ -53,3 +62,4 @@ export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index`; export const SIGNALS_INDEX_KEY = 'signalsIndex'; export const DETECTION_ENGINE_SIGNALS_URL = `${DETECTION_ENGINE_URL}/signals`; export const DETECTION_ENGINE_SIGNALS_STATUS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/status`; +export const DETECTION_ENGINE_QUERY_SIGNALS_URL = `${DETECTION_ENGINE_SIGNALS_URL}/search`; diff --git a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts index 8a9477ad67901c..b2b8ce7b9c0003 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/lib/login/helpers.ts @@ -39,7 +39,7 @@ const ELASTICSEARCH_PASSWORD = 'ELASTICSEARCH_PASSWORD'; /** * The Kibana server endpoint used for authentication */ -const LOGIN_API_ENDPOINT = '/api/security/v1/login'; +const LOGIN_API_ENDPOINT = '/internal/security/login'; /** * Authenticates with Kibana using, if specified, credentials specified by @@ -68,7 +68,7 @@ const credentialsProvidedByEnvironment = (): boolean => * Authenticates with Kibana by reading credentials from the * `CYPRESS_ELASTICSEARCH_USERNAME` and `CYPRESS_ELASTICSEARCH_PASSWORD` * environment variables, and POSTing the username and password directly to - * Kibana's `security/v1/login` endpoint, bypassing the login page (for speed). + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). */ const loginViaEnvironmentCredentials = () => { cy.log( @@ -90,7 +90,7 @@ const loginViaEnvironmentCredentials = () => { /** * Authenticates with Kibana by reading credentials from the * `kibana.dev.yml` file and POSTing the username and password directly to - * Kibana's `security/v1/login` endpoint, bypassing the login page (for speed). + * Kibana's `/internal/security/login` endpoint, bypassing the login page (for speed). */ const loginViaConfig = () => { cy.log( diff --git a/x-pack/legacy/plugins/siem/package.json b/x-pack/legacy/plugins/siem/package.json index ef6431327b5ab4..bf5d6d3a3089cb 100644 --- a/x-pack/legacy/plugins/siem/package.json +++ b/x-pack/legacy/plugins/siem/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "lodash": "^4.17.15", - "react-beautiful-dnd": "^12.1.1", + "react-beautiful-dnd": "^12.2.0", "react-markdown": "^4.0.6" } } diff --git a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx index c46fd88b571907..2321b06c07cc0a 100644 --- a/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/bytes/index.test.tsx @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; import { PreferenceFormattedBytes } from '../formatted_bytes'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Bytes } from '.'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('Bytes', () => { + const mount = useMountAppended(); + test('it renders the expected formatted bytes', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx index 0433176475e0ac..b0c165fedfffc1 100644 --- a/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/certificate_fingerprint/index.test.tsx @@ -4,14 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { CertificateFingerprint } from '.'; describe('CertificateFingerprint', () => { + const mount = useMountAppended(); test('renders the expected label', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx index 008ece5c7e69c2..4b546bca1f72eb 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -14,10 +14,13 @@ import { TestProviders } from '../../mock'; import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DraggableWrapper } from './draggable_wrapper'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('DraggableWrapper', () => { const dataProvider = mockDataProviders[0]; const message = 'draggable wrapper content'; + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against the snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx index 39abbdd4d4e382..056669673bb9e2 100644 --- a/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -14,8 +14,11 @@ import { TestProviders } from '../../mock'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { DroppableWrapper } from './droppable_wrapper'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('DroppableWrapper', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against the snapshot', () => { const message = 'draggable wrapper content'; diff --git a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx index d3dcba9526bddc..f1ed533bef545f 100644 --- a/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/draggables/index.test.tsx @@ -7,10 +7,10 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../mock'; import { getEmptyString } from '../empty_value'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { DefaultDraggable, @@ -20,6 +20,8 @@ import { } from '.'; describe('draggables', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default DefaultDraggable', () => { const wrapper = shallow( @@ -99,7 +101,7 @@ describe('draggables', () => { describe('DefaultDraggable', () => { test('it works with just an id, field, and value and is some value', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -122,7 +124,7 @@ describe('draggables', () => { }); test('it renders a tooltip with the field name if a tooltip is not explicitly provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -137,7 +139,7 @@ describe('draggables', () => { }); test('it renders the tooltipContent when a string is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when an element is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it does NOT render a tooltip when tooltipContent is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('DraggableBadge', () => { test('it works with just an id, field, and value and is the default', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns Empty string text if value is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders a tooltip with the field name if a tooltip is not explicitly provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when a string is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the tooltipContent when an element is provided as content', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it does NOT render a tooltip when tooltipContent is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected formatted duration', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap index fb896059460b90..6794aab2057036 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap @@ -16,7 +16,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = `

beats , + "defaultIndex": + siem:defaultIndex + , "example": ./packetbeat setup , @@ -39,7 +46,7 @@ exports[`IndexPatternsMissingPrompt renders correctly against snapshot 1`] = `

diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx index 1e29676415d79f..6533be49c3430f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx @@ -21,9 +21,18 @@ export const IndexPatternsMissingPromptComponent = () => ( <>

+ {'siem:defaultIndex'} + + ), beats: ( (

diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 5e1eae1649b41a..929b4983b5fd76 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { FeatureProperty } from '../types'; @@ -12,6 +12,7 @@ import { getRenderedFieldValue, PointToolTipContentComponent } from './point_too import { TestProviders } from '../../../mock'; import { getEmptyStringTag } from '../../empty_value'; import { HostDetailsLink, IPDetailsLink } from '../../links'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../search_bar', () => ({ siemFilterManager: { @@ -20,6 +21,8 @@ jest.mock('../../search_bar', () => ({ })); describe('PointToolTipContent', () => { + const mount = useMountAppended(); + const mockFeatureProps: FeatureProperty[] = [ { _propertyKey: 'host.name', diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx index d8c0e46d8480bf..f1e96392d6afcb 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_details.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,10 +14,13 @@ import { TestProviders } from '../../mock/test_providers'; import { EventDetails } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EventDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('should match snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx index f2fac81669d162..2c28ab8696f0ef 100644 --- a/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/event_details/event_fields_browser.test.tsx @@ -5,7 +5,6 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { mockDetailItemData, mockDetailItemDataId } from '../../mock/mock_detail_item'; import { TestProviders } from '../../mock/test_providers'; @@ -13,14 +12,17 @@ import { TestProviders } from '../../mock/test_providers'; import { EventFieldsBrowser } from './event_fields_browser'; import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('EventFieldsBrowser', () => { + const mount = useMountAppended(); + describe('column headers', () => { ['Field', 'Value', 'Description'].forEach(header => { test(`it renders the ${header} column header`, () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('filter input', () => { test('it renders a filter input with the expected placeholder', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { test('it renders an UNchecked checkbox for a field that is not a member of columnHeaders', () => { const field = 'agent.id'; - const wrapper = mountWithIntl( + const wrapper = mount( { test('it renders an checked checkbox for a field that is a member of columnHeaders', () => { const field = '@timestamp'; - const wrapper = mountWithIntl( + const wrapper = mount( { const field = '@timestamp'; const toggleColumn = jest.fn(); - const wrapper = mountWithIntl( + const wrapper = mount( { describe('field type icon', () => { test('it renders the expected icon type for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('field', () => { test('it renders the field name for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('value', () => { test('it renders the expected value for the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('description', () => { test('it renders the expected field description the data provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the "Showing..." subtitle with the expected event count', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx index d3113948706830..e46153c18c2b5e 100644 --- a/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/events_viewer/index.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -12,6 +11,7 @@ import { useKibanaCore } from '../../lib/compose/kibana_core'; import { wait } from '../../lib/helpers'; import { mockIndexPattern, TestProviders } from '../../mock'; import { mockUiSettings } from '../../mock/ui_settings'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; @@ -40,6 +40,8 @@ const from = 1566943856794; const to = 1566857456791; describe('StatefulEventsViewer', () => { + const mount = useMountAppended(); + test('it renders the events viewer', async () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx index 2d69db82405bae..e45f5dacb36a26 100644 --- a/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/field_renderers/field_renderers.test.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../graphql/types'; import { TestProviders } from '../../mock'; @@ -25,10 +24,13 @@ import { MoreContainer, } from './field_renderers'; import { mockData } from '../page/network/ip_overview/mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; type AutonomousSystem = GetIpOverviewQuery.AutonomousSystem; describe('Field Renderers', () => { + const mount = useMountAppended(); + describe('#locationRenderer', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( @@ -184,7 +186,7 @@ describe('Field Renderers', () => { describe('#whoisRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl(whoisRenderer('10.10.10.10')); + const wrapper = shallow(whoisRenderer('10.10.10.10')); expect(toJson(wrapper)).toMatchSnapshot(); }); @@ -192,9 +194,7 @@ describe('Field Renderers', () => { describe('#reputationRenderer', () => { test('it renders correctly against snapshot', () => { - const wrapper = shallowWithIntl( - {reputationRenderer('10.10.10.10')} - ); + const wrapper = shallow({reputationRenderer('10.10.10.10')}); expect(toJson(wrapper.find('DragDropContext'))).toMatchSnapshot(); }); @@ -202,7 +202,7 @@ describe('Field Renderers', () => { describe('DefaultFieldRenderer', () => { test('it should render a single item', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -211,7 +211,7 @@ describe('Field Renderers', () => { }); test('it should render two items', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all items when the item count exactly equals displayCount', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all items up to displayCount and the expected "+ n More" popover anchor text for items greater than displayCount', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { const rowItems = ['item1', 'item2', 'item3', 'item4', 'item5', 'item6', 'item7']; test('it should only render the items after overflowIndexStart', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should render all the items when overflowIndexStart is zero', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should have the overflow `auto` style to enable scrolling when necessary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it should use the moreMaxHeight prop as the value for the max-height style', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { test('it should only invoke the optional render function, when provided, for the items after overflowIndexStart', () => { const render = jest.fn(); - mountWithIntl( + mount( { const timelineId = 'test'; const selectedCategoryId = 'client'; + const mount = useMountAppended(); test('it renders the category id as the value of the title', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx index a569fc42e550fd..6034f5a4764432 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/field_items.test.tsx @@ -5,7 +5,6 @@ */ import { omit } from 'lodash/fp'; -import { mount } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; @@ -17,6 +16,7 @@ import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/helpers'; import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; +import { useMountAppended } from '../../utils/use_mount_appended'; const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; @@ -37,6 +37,7 @@ const columnHeaders: ColumnHeader[] = [ describe('field_items', () => { const timelineId = 'test'; + const mount = useMountAppended(); describe('getFieldItems', () => { Object.keys(selectedCategoryFields!).forEach(fieldId => { diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx index 2193d0c661fb79..68ba2e2774314e 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/fields_pane.test.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { FIELDS_PANE_WIDTH } from './helpers'; import { FieldsPane } from './fields_pane'; @@ -16,6 +16,8 @@ import { FieldsPane } from './fields_pane'; const timelineId = 'test'; describe('FieldsPane', () => { + const mount = useMountAppended(); + test('it renders the selected category', () => { const selectedCategory = 'auditd'; diff --git a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx index 54847cda281f49..8a01a01b1daae8 100644 --- a/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/fields_browser/index.test.tsx @@ -14,6 +14,19 @@ import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; import { StatefulFieldsBrowserComponent } from '.'; +// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + describe('StatefulFieldsBrowser', () => { const timelineId = 'test'; diff --git a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx index acea2d1cce468b..66e9bc700b3a16 100644 --- a/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/flyout/pane/index.test.tsx @@ -19,6 +19,7 @@ const testWidth = 640; const usersViewing = ['elastic']; const mockUseKibanaCore = useKibanaCore as jest.Mock; +jest.mock('ui/new_platform'); jest.mock('../../../lib/compose/kibana_core'); mockUseKibanaCore.mockImplementation(() => ({ uiSettings: mockUiSettings, diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx index c20f3c7185e664..5644a344f91d6d 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.test.tsx @@ -5,17 +5,20 @@ */ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import React from 'react'; import { TestProviders } from '../../mock'; import '../../mock/ui_settings'; import { HeaderPage } from './index'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('HeaderPage', () => { + const mount = useMountAppended(); + test('it renders', () => { const wrapper = shallow( { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -22,7 +24,7 @@ describe('Port', () => { }); test('it renders the the ip address', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -37,7 +39,7 @@ describe('Port', () => { }); test('it hyperlinks to the network/ip page', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx index 2cb564aeed710b..3842b7be678767 100644 --- a/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ja3_fingerprint/index.test.tsx @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; import { TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Ja3Fingerprint } from '.'; describe('Ja3Fingerprint', () => { + const mount = useMountAppended(); + test('renders the expected label', () => { const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx index c23a757647a42a..e2ada4682fdec5 100644 --- a/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/last_event_time/index.test.tsx @@ -10,12 +10,12 @@ import { getEmptyValue } from '../empty_value'; import { LastEventIndexKey } from '../../graphql/types'; import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; import { TestProviders } from '../../mock'; import '../../mock/ui_settings'; import { LastEventTime } from '.'; -import { mount } from 'enzyme'; const mockUseLastEventTimeQuery: jest.Mock = useLastEventTimeQuery as jest.Mock; jest.mock('../../containers/events/last_event_time', () => ({ @@ -23,6 +23,8 @@ jest.mock('../../containers/events/last_event_time', () => ({ })); describe('Last Event Time Stat', () => { + const mount = useMountAppended(); + beforeEach(() => { mockUseLastEventTimeQuery.mockReset(); }); diff --git a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx index c3268270919e28..80ccd07c30249b 100644 --- a/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/markdown/markdown_hint.test.tsx @@ -10,7 +10,7 @@ import * as React from 'react'; import { MarkdownHintComponent } from './markdown_hint'; -describe.skip('MarkdownHintComponent ', () => { +describe('MarkdownHintComponent ', () => { test('it has inline visibility when show is true', () => { const wrapper = mount(); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx index c401075af42ce2..562e3c15675a7f 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/entity_draggable.test.tsx @@ -6,11 +6,14 @@ import React from 'react'; import toJson from 'enzyme-to-json'; -import { shallow, mount } from 'enzyme'; +import { shallow } from 'enzyme'; import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('entity_draggable', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( { let anomalies: Anomalies = cloneDeep(mockAnomalies); + const mount = useMountAppended(); + beforeEach(() => { anomalies = cloneDeep(mockAnomalies); }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx index 5bd11169e48408..f01df381384569 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; import { getEmptyValue } from '../../empty_value'; import { Anomalies } from '../types'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const endDate: number = new Date('3000-01-01T00:00:00.000Z').valueOf(); const narrowDateRange = jest.fn(); @@ -21,6 +22,7 @@ jest.mock('../../../lib/settings/use_kibana_ui_setting'); describe('anomaly_scores', () => { let anomalies: Anomalies = cloneDeep(mockAnomalies); + const mount = useMountAppended(); beforeEach(() => { anomalies = cloneDeep(mockAnomalies); diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 04fed8e4fff3f0..80980756d21305 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -10,8 +10,8 @@ import * as i18n from './translations'; import { AnomaliesByHost, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; import { TestProviders } from '../../../mock'; -import { mount } from 'enzyme'; import React from 'react'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).valueOf(); const endDate = new Date(3000).valueOf(); @@ -19,6 +19,8 @@ const interval = 'days'; const narrowDateRange = jest.fn(); describe('get_anomalies_host_table_columns', () => { + const mount = useMountAppended(); + test('on hosts page, we expect to get all columns', () => { expect( getAnomaliesHostTableColumnsCurated( diff --git a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 768c7af8f4b2c9..b27ccaf1ca7de6 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -9,9 +9,9 @@ import { NetworkType } from '../../../store/network/model'; import * as i18n from './translations'; import { AnomaliesByNetwork, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; -import { mount } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).valueOf(); const endDate = new Date(3000).valueOf(); @@ -19,6 +19,8 @@ const interval = 'days'; const narrowDateRange = jest.fn(); describe('get_anomalies_network_table_columns', () => { + const mount = useMountAppended(); + test('on network page, we expect to get all columns', () => { expect( getAnomaliesNetworkTableColumnsCurated( diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx index 4ea9e0cdafacb2..1a8360fe82c584 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/ml_popover.test.tsx @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import * as React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + import { MlPopover } from './ml_popover'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -16,7 +17,7 @@ jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ describe('MlPopover', () => { test('shows upgrade popover on mouse click', () => { - const wrapper = mount(); + const wrapper = mountWithIntl(); // TODO: Update to use act() https://fb.me/react-wrap-tests-with-act wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index fbeb1a2090cfde..d7061ba4efd9cb 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -15,6 +15,7 @@ import { HostsTableType } from '../../store/hosts/model'; import { RouteSpyState } from '../../utils/route/types'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; +jest.mock('ui/new_platform'); jest.mock('./breadcrumbs', () => ({ setBreadcrumbs: jest.fn(), })); diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx index e84e3066e4f695..00b1d4c066d4a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx @@ -16,6 +16,8 @@ import { CONSTANTS } from '../../url_state/constants'; import { TabNavigationComponent } from './'; import { TabNavigationProps } from './types'; +jest.mock('ui/new_platform'); + describe('Tab Navigation', () => { const pageName = SiemPageName.hosts; const hostName = 'siem-window'; diff --git a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx index 2c5152535a3701..2d8c201e41462f 100644 --- a/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/netflow/index.test.tsx @@ -8,7 +8,6 @@ import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; import * as React from 'react'; import { shallow } from 'enzyme'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; @@ -56,6 +55,7 @@ import { NETWORK_PROTOCOL_FIELD_NAME, NETWORK_TRANSPORT_FIELD_NAME, } from '../source_destination/field_names'; +import { useMountAppended } from '../../utils/use_mount_appended'; jest.mock('../../lib/settings/use_kibana_ui_setting'); @@ -121,13 +121,15 @@ const getNetflowInstance = () => ( ); describe('Netflow', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow(getNetflowInstance()); expect(toJson(wrapper)).toMatchSnapshot(); }); test('it renders a destination label', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -138,7 +140,7 @@ describe('Netflow', () => { }); test('it renders destination.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -149,7 +151,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.continent_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -160,7 +162,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.country_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -171,7 +173,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.country_iso_code', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -182,7 +184,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.region_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -193,7 +195,7 @@ describe('Netflow', () => { }); test('it renders destination.geo.city_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -204,7 +206,7 @@ describe('Netflow', () => { }); test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -215,7 +217,7 @@ describe('Netflow', () => { }); test('it renders destination.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -226,7 +228,7 @@ describe('Netflow', () => { }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -240,7 +242,7 @@ describe('Netflow', () => { }); test('it renders event.duration', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -251,7 +253,7 @@ describe('Netflow', () => { }); test('it renders event.end', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -262,7 +264,7 @@ describe('Netflow', () => { }); test('it renders event.start', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -273,7 +275,7 @@ describe('Netflow', () => { }); test('it renders network.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -284,7 +286,7 @@ describe('Netflow', () => { }); test('it renders network.community_id', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -295,7 +297,7 @@ describe('Netflow', () => { }); test('it renders network.direction', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -306,7 +308,7 @@ describe('Netflow', () => { }); test('it renders network.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -317,7 +319,7 @@ describe('Netflow', () => { }); test('it renders network.protocol', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -328,7 +330,7 @@ describe('Netflow', () => { }); test('it renders process.name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -339,7 +341,7 @@ describe('Netflow', () => { }); test('it renders a source label', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -350,7 +352,7 @@ describe('Netflow', () => { }); test('it renders source.bytes', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -361,7 +363,7 @@ describe('Netflow', () => { }); test('it renders source.geo.continent_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -372,7 +374,7 @@ describe('Netflow', () => { }); test('it renders source.geo.country_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -383,7 +385,7 @@ describe('Netflow', () => { }); test('it renders source.geo.country_iso_code', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -394,7 +396,7 @@ describe('Netflow', () => { }); test('it renders source.geo.region_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -405,7 +407,7 @@ describe('Netflow', () => { }); test('it renders source.geo.city_name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -416,7 +418,7 @@ describe('Netflow', () => { }); test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -427,7 +429,7 @@ describe('Netflow', () => { }); test('it renders source.packets', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -438,7 +440,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.client_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -452,7 +454,7 @@ describe('Netflow', () => { }); test('renders tls.client_certificate.fingerprint.sha1 text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -464,7 +466,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -475,7 +477,7 @@ describe('Netflow', () => { }); test('renders tls.fingerprints.ja3.hash text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -486,7 +488,7 @@ describe('Netflow', () => { }); test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -500,7 +502,7 @@ describe('Netflow', () => { }); test('renders tls.server_certificate.fingerprint.sha1 text', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -512,7 +514,7 @@ describe('Netflow', () => { }); test('it renders network.transport', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper @@ -523,7 +525,7 @@ describe('Netflow', () => { }); test('it renders user.name', () => { - const wrapper = mountWithIntl({getNetflowInstance()}); + const wrapper = mount({getNetflowInstance()}); expect( wrapper diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx index 234d5ac959c8cb..6c3ab048492366 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx @@ -7,7 +7,7 @@ import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { render } from '@testing-library/react'; +import { render, act } from '@testing-library/react'; import { mockFirstLastSeenHostQuery } from '../../../../containers/hosts/first_last_seen/mock'; import { wait } from '../../../../lib/helpers'; @@ -18,6 +18,16 @@ import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; jest.mock('../../../../lib/settings/use_kibana_ui_setting'); +// Suppress warnings about "react-apollo" until we migrate to apollo@3 +/* eslint-disable no-console */ +const originalError = console.error; +beforeAll(() => { + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; +}); + describe('FirstLastSeen Component', () => { const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; @@ -44,7 +54,7 @@ describe('FirstLastSeen Component', () => { ); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${firstSeen}
` @@ -59,7 +69,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${lastSeen}
` ); @@ -76,7 +86,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${lastSeen}
` @@ -94,7 +104,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.innerHTML).toBe( `
${firstSeen}
` @@ -111,7 +121,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.textContent).toBe('something-invalid'); }); @@ -125,7 +135,7 @@ describe('FirstLastSeen Component', () => {
); - await wait(); + await act(() => wait()); expect(container.textContent).toBe('something-invalid'); }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx index c9fdce94f780ac..8c27f86d78884f 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -16,6 +16,7 @@ import { mockGlobalState, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { mockUiSettings } from '../../../../mock/ui_settings'; import { useKibanaCore } from '../../../../lib/compose/kibana_core'; import { createStore, hostsModel, State } from '../../../../store'; @@ -43,6 +44,7 @@ describe('Hosts Table', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); @@ -71,28 +73,7 @@ describe('Hosts Table', () => { }); describe('Sorting on Table', () => { - let wrapper = mount( - - - - - - ); + let wrapper: ReturnType; beforeEach(() => { wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx index 10d9eb618c7661..28ddb1df12c3a2 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import * as React from 'react'; import { TestProviders } from '../../../../mock'; import { hostsModel } from '../../../../store'; import { getEmptyValue } from '../../../empty_value'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { getArgs, UncommonProcessTable, getUncommonColumnsCurated } from '.'; import { mockData } from './mock'; @@ -20,6 +21,7 @@ import * as i18n from './translations'; describe('Uncommon Process Table Component', () => { const loadPage = jest.fn(); + const mount = useMountAppended(); describe('rendering', () => { test('it renders the default Uncommon process table', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx index 8bf338d17c47bc..0537b95ca6cf75 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; import { createStore, networkModel, State } from '../../../../store'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { NetworkDnsTable } from '.'; import { mockData } from './mock'; @@ -22,8 +23,8 @@ jest.mock('../../../../lib/settings/use_kibana_ui_setting'); describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx index c92661a909a6e1..50d64817f81f8f 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_http_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkHttpTable } from '.'; @@ -24,6 +25,7 @@ describe('NetworkHttp Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx index ca7a3c0bb4387e..eb4179a0404314 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -18,14 +18,17 @@ import { mockIndexPattern, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkTopCountriesTable } from '.'; import { mockData } from './mock'; + jest.mock('../../../../lib/settings/use_kibana_ui_setting'); describe('NetworkTopCountries Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; + const mount = useMountAppended(); let store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx index 884825422beb0a..3157847b323768 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -18,16 +18,20 @@ import { mockIndexPattern, TestProviders, } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { NetworkTopNFlowTable } from '.'; import { mockData } from './mock'; + jest.mock('../../../../lib/settings/use_kibana_ui_setting'); + describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx index 8c397053380c5c..4313c455a0df1a 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/tls_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -12,6 +12,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { TlsTable } from '.'; @@ -24,6 +25,7 @@ describe('Tls Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx index d178164fd3fd73..d6b9ec24de0aa0 100644 --- a/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/page/network/users_table/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { getOr } from 'lodash/fp'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { Provider as ReduxStoreProvider } from 'react-redux'; import { FlowTarget } from '../../../../graphql/types'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { createStore, networkModel, State } from '../../../../store'; import { UsersTable } from '.'; @@ -31,6 +32,7 @@ describe('Users Table Component', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); + const mount = useMountAppended(); beforeEach(() => { store = createStore(state, apolloClientObservable); diff --git a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx index 3e4db72cf55a15..330385e39ca79f 100644 --- a/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/port/index.test.tsx @@ -7,13 +7,15 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../mock/test_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { Port } from '.'; describe('Port', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( @@ -22,7 +24,7 @@ describe('Port', () => { }); test('it renders the port', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -37,7 +39,7 @@ describe('Port', () => { }); test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -54,7 +56,7 @@ describe('Port', () => { }); test('it renders an external link', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx index ce102d7ade53b4..10b769e2a791c3 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.test.tsx @@ -7,8 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; import { uiSettingsServiceMock } from '../../../../../../../src/core/public/ui_settings/ui_settings_service.mock'; import { useKibanaCore } from '../../lib/compose/kibana_core'; import { TestProviders, mockIndexPattern } from '../../mock'; diff --git a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx index bf440e238c2a32..9f706790bec672 100644 --- a/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/query_bar/index.tsx @@ -8,7 +8,6 @@ import { isEqual } from 'lodash/fp'; import React, { memo, useState, useEffect, useMemo, useCallback } from 'react'; import { IndexPattern } from 'ui/index_patterns'; -import { SavedQuery, SearchBar } from '../../../../../../../src/legacy/core_plugins/data/public'; import { esFilters, IIndexPattern, @@ -16,6 +15,8 @@ import { Query, TimeHistory, TimeRange, + SavedQuery, + SearchBar, SavedQueryTimeFilter, } from '../../../../../../../src/plugins/data/public'; import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index fa9ff1e16ddb75..3d02cff7b72e87 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -16,7 +16,6 @@ import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { OnTimeChangeProps } from '@elastic/eui'; import { npStart } from 'ui/new_platform'; -import { start as data } from '../../../../../../../src/legacy/core_plugins/data/public/legacy'; import { inputsActions } from '../../store/inputs'; import { InputsRange } from '../../store/inputs/model'; @@ -37,12 +36,9 @@ import { import { timelineActions, hostsActions, networkActions } from '../../store/actions'; import { TimeRange, Query, esFilters } from '../../../../../../../src/plugins/data/public'; -const { - ui: { SearchBar }, -} = data; - export const siemFilterManager = npStart.plugins.data.query.filterManager; export const savedQueryService = npStart.plugins.data.query.savedQueries; +const { SearchBar } = npStart.plugins.data.ui; interface SiemSearchBarRedux { end: number; diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx index edce246aa61bdc..1679795951a560 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/index.test.tsx @@ -5,7 +5,7 @@ */ import numeral from '@elastic/numeral'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { get } from 'lodash/fp'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { asArrayIfExists } from '../../lib/helpers'; import { getMockNetflowData } from '../../mock'; import { TestProviders } from '../../mock/test_providers'; import { ID_FIELD_NAME } from '../event_details/event_id'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; import { @@ -98,6 +99,8 @@ const getSourceDestinationInstance = () => ( ); describe('SourceDestination', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow(
{getSourceDestinationInstance()}
); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx index c9fff7328165ca..d42d34c85a4dac 100644 --- a/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import { get } from 'lodash/fp'; import * as React from 'react'; @@ -15,6 +14,7 @@ import { ID_FIELD_NAME } from '../event_details/event_id'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; import * as i18n from '../timeline/body/renderers/translations'; +import { useMountAppended } from '../../utils/use_mount_appended'; import { getPorts, @@ -38,6 +38,8 @@ import { jest.mock('../../lib/settings/use_kibana_ui_setting'); describe('SourceDestinationIp', () => { + const mount = useMountAppended(); + describe('#isIpFieldPopulated', () => { test('it returns true when type is `source` and sourceIp has an IP address', () => { expect( diff --git a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx index 9d9087d34a7659..d864d4306b8ef6 100644 --- a/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/tables/helpers.test.tsx @@ -11,13 +11,15 @@ import { OverflowFieldComponent, } from './helpers'; import * as React from 'react'; -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; +import { useMountAppended } from '../../utils/use_mount_appended'; describe('Table Helpers', () => { const items = ['item1', 'item2', 'item3']; + const mount = useMountAppended(); describe('#getRowItemDraggable', () => { test('it returns correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx index 370f864f51f3c7..b537499739d586 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { Direction } from '../../../../graphql/types'; import { mockBrowserFields } from '../../../../../public/containers/source/mock'; import { Sort } from '../sort'; import { TestProviders } from '../../../../mock/test_providers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { ColumnHeadersComponent } from '.'; @@ -26,6 +27,8 @@ jest.mock('../../../resize_handle/is_resizing', () => ({ })); describe('ColumnHeaders', () => { + const mount = useMountAppended(); + describe('rendering', () => { const sort: Sort = { columnId: 'fooColumn', diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx index 0fb4c4f375684c..a4ed5571bb0da8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/index.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, ReactWrapper } from 'enzyme'; import * as React from 'react'; import { mockBrowserFields } from '../../../containers/source/mock'; @@ -16,6 +15,7 @@ import { Body, BodyProps } from '.'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; import { wait } from '../../../lib/helpers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../lib/settings/use_kibana_ui_setting'); @@ -40,6 +40,8 @@ jest.mock('../../../lib/helpers/scheduler', () => ({ })); describe('Body', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the column headers', () => { const wrapper = mount( @@ -205,7 +207,7 @@ describe('Body', () => { const dispatchAddNoteToEvent = jest.fn(); const dispatchOnPinEvent = jest.fn(); - const addaNoteToEvent = (wrapper: ReactWrapper, note: string) => { + const addaNoteToEvent = (wrapper: ReturnType, note: string) => { wrapper .find('[data-test-subj="add-note"]') .first() diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx index dbf6db6cd2bd92..ad904554e33ad3 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/args.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { TestProviders } from '../../../../mock'; import { ArgsComponent } from './args'; describe('Args', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -27,7 +29,7 @@ describe('Args', () => { }); test('it returns an empty string when both args and process title are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when both args and process title are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when args is an empty array, and title is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -64,7 +66,7 @@ describe('Args', () => { }); test('it returns args when args are provided, and process title is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process title when process title is provided, and args is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns both args and process title, when both are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default AuditAcquiredCredsDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('GenericDetails', () => { }); test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#AuditdConnectedToLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just a session if only given an id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a process name if only given a process name and id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns session, user name, and process title if process title with id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default GenericFileDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -33,7 +35,7 @@ describe('GenericFileDetails', () => { }); test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#AuditdGenericFileLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just session if only session id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a process name if only given a process name and id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns session user name and title if process title with id is given', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('#createGenericAuditRowRenderer', () => { let nonAuditd: Ecs; let auditd: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index b8da9d50402bf6..a5b861be08e563 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; describe('UserPrimarySecondary', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default PrimarySecondaryUserInfo', () => { const wrapper = shallow( @@ -28,7 +30,7 @@ describe('UserPrimarySecondary', () => { }); test('should render user name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render user name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render primary name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render primary name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the secondary name only if that is all that is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the secondary name only if the others are in unset mode', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the user name if all three are the same', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('should render the primary with as if all three are different', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SessionUserHostWorkingDir', () => { const wrapper = shallow( @@ -34,7 +36,7 @@ describe('SessionUserHostWorkingDir', () => { }); test('it renders with just eventId and contextId', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName, primary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with only eventId, contextId, session, hostName, userName, primary, secondary', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders with everything as expected', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text given an Endgame DNS request_event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text when all properties are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsQuestionName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsQuestionType is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsResolvedIp is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when dnsResponseCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when hostName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processExecutable is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processPid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userDomain is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when winlogEventId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when both eventCode and winlogEventId are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; + const mount = useMountAppended(); + beforeEach(() => { mockDatum = cloneDeep(mockTimelineData[0].data); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index e1da17ad2904b3..77569f07a23c23 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -10,7 +10,6 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; @@ -20,12 +19,15 @@ import { mockEndgameUserLogon, mockEndgameUserLogoff, } from '../../../../../../public/mock/mock_endgame_ecs_data'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { EndgameSecurityEventDetails } from './endgame_security_event_details'; describe('EndgameSecurityEventDetails', () => { + const mount = useMountAppended(); + test('it renders the expected text given an Endgame Security user_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security admin_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security explicit_user_logon event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text given an Endgame Security user_logoff event', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text when all properties are provided and event action is admin_logon', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when all properties are provided and event action is explicit_user_logon', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameLogonType is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameSubjectDomainName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameSubjectLogonId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when when endgameSubjectUserName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetDomainName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetLogonId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when endgameTargetUserName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventAction is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when eventCode is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when hostName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processExecutable is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when processPid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userDomain is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when userName is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when winlogEventId is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when BOTH eventCode and winlogEventId are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('it renders the expected text and exit code, when both text and an endgameExitCode are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -22,7 +24,7 @@ describe('ExitCodeDraggable', () => { }); test('it returns an empty string when text is provided, but endgameExitCode is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when text is provided, but endgameExitCode is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when text is provided, but endgameExitCode is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -59,7 +61,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -68,7 +70,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -77,7 +79,7 @@ describe('ExitCodeDraggable', () => { }); test('it renders just the exit code when text is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx index 80be9fd339f51b..ff63d02acc37c7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx @@ -5,15 +5,17 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { FileDraggable } from './file_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('FileDraggable', () => { + const mount = useMountAppended(); + test('it prefers fileName and filePath over endgameFileName and endgameFilePath when all of them are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns an empty string when none of the files or paths are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders just the endgameFileName if only endgameFileName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders "in endgameFilePath" if only endgameFilePath is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders just the filename if only fileName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders "in filePath" if only filePath is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const mount = useMountAppended(); test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx index 1b22d318e19cd3..d445ec2859e2c2 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -18,10 +18,13 @@ import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '.'; import { getColumnRenderer } from './get_column_renderer'; import { getValues, findItem, deleteItemIdx } from './helpers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('get_column_renderer', () => { let nonSuricata: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; + const mount = useMountAppended(); + beforeEach(() => { nonSuricata = cloneDeep(mockTimelineData[0].data); }); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx index f7a5462be6e8fc..bea525116021d5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash'; import * as React from 'react'; @@ -13,6 +13,7 @@ import { mockBrowserFields } from '../../../../containers/source/mock'; import { Ecs } from '../../../../graphql/types'; import { mockTimelineData } from '../../../../mock'; import { TestProviders } from '../../../../mock/test_providers'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { rowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; @@ -23,6 +24,8 @@ describe('get_column_renderer', () => { let zeek: Ecs; let system: Ecs; let auditd: Ecs; + const mount = useMountAppended(); + beforeEach(() => { nonSuricata = cloneDeep(mockTimelineData[0].ecs); suricata = cloneDeep(mockTimelineData[2].ecs); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx index 449ed37dbeb30b..6c58b1ec6f35c9 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx @@ -4,15 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; - -import * as React from 'react'; +import React from 'react'; import { mockTimelineData, TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; import { HostWorkingDir } from './host_working_dir'; describe('HostWorkingDir', () => { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const wrapper = shallow( { + const mount = useMountAppended(); + test('renders correctly against snapshot', () => { const browserFields: BrowserFields = {}; const children = netflowRowRenderer.renderRow({ diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx index 9f6a8676694e40..80ae10a48415cf 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -5,15 +5,17 @@ */ import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { ParentProcessDraggable } from './parent_process_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('ParentProcessDraggable', () => { + const mount = useMountAppended(); + test('displays the text, endgameParentProcessName, and processPpid when they are all provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays nothing when the text is provided, but endgameParentProcessName and processPpid are both undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the text and processPpid when endgameParentProcessName is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the processPpid when both endgameParentProcessName and text are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the text and endgameParentProcessName when processPpid is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays the endgameParentProcessName when both processPpid and text are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + describe('rendering', () => { let mockDatum: TimelineNonEcsData[]; const _id = mockTimelineData[0]._id; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx index b087351107c77c..ff1cb60db0d93f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../mock'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; +import { useMountAppended } from '../../../../utils/use_mount_appended'; describe('ProcessDraggable', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -60,7 +62,7 @@ describe('ProcessDraggable', () => { }); test('it returns process name if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process executable if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid if that is all that is passed in', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just process name if process.pid and endgame.pid are NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns just process executable if process.pid and endgame.pid are NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process executable if everything else is an empty string or NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.process_name if everything else is an empty string or NaN', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.process_name and endgame.pid if everything else is an empty string or undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid if everything else is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame.pid if everything else is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns pid and process name if everything is filled', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable and if process name and endgame process name are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns endgame pid and executable and if process name and endgame process name are null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable and if process name is undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns process pid and executable if process name is an empty string', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it prefers process.name when process.executable and endgame.process_name are also provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering process.executable when process.name is NOT provided, but process.executable and endgame.process_name are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering endgame.process_name when process.name and process.executable are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it prefers process.pid when endgame.pid is also provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it falls back to rendering endgame.pid when process.pid is NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); describe('ProcessDraggableWithNonExistentProcess', () => { + const mount = useMountAppended(); + test('it renders the expected text when all fields are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just endgamePid is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just endgameProcessName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processExecutable is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processName is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when just processPid is provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it renders the expected text when all values are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); + test('displays the processHashMd5, processHashSha1, and processHashSha256 when they are all provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays nothing when processHashMd5, processHashSha1, and processHashSha256 are all undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashMd5 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashSha1 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('displays just processHashSha256 when the other hashes are undefined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 20b64661b6a003..3f77726474c568 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -7,14 +7,16 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData } from '../../../../../mock'; import { TestProviders } from '../../../../../mock/test_providers'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; describe('SuricataDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SuricataDetails', () => { const wrapper = shallow( @@ -28,7 +30,7 @@ describe('SuricataDetails', () => { }); test('it returns text if the data does contain suricata data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); let nonSuricata: Ecs; let suricata: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index 2278ed135e5e29..4eefb4b0bc8b9a 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { SuricataSignature, Tokens, @@ -18,6 +18,8 @@ import { } from './suricata_signature'; describe('SuricataSignature', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SuricataSignature', () => { const wrapper = shallow( @@ -39,7 +41,7 @@ describe('SuricataSignature', () => { }); test('should render a single if it is present', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
@@ -48,7 +50,7 @@ describe('SuricataSignature', () => { }); test('should render the multiple tokens if they are present', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
@@ -59,7 +61,7 @@ describe('SuricataSignature', () => { describe('DraggableSignatureId', () => { test('it renders the default SuricataSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -68,7 +70,7 @@ describe('SuricataSignature', () => { }); test('it renders a tooltip for the signature field', () => { - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx index b85bef4d5ac365..632e8ff35950e8 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -115,7 +115,6 @@ export const SuricataSignature = React.memo<{ data-test-subj="draggable-signature-link" field={SURICATA_SIGNATURE_FIELD_NAME} id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} - name={name} value={signature} >
diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx index ee58111dd5709e..54f5b2f1652874 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx @@ -7,14 +7,16 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { BrowserFields } from '../../../../../containers/source'; import { mockBrowserFields } from '../../../../../containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../mock'; import { SystemGenericDetails, SystemGenericLine } from './generic_details'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; describe('SystemGenericDetails', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SystemGenericDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('SystemGenericDetails', () => { }); test('it returns system rendering if the data does contain system data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#SystemGenericLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns nothing if data is all null', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return only the host name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default SystemGenericDetails', () => { // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy @@ -32,7 +34,7 @@ describe('SystemGenericFileDetails', () => { }); test('it returns system rendering if the data does contain system data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#SystemGenericFileLine', () => { test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns nothing if data is all null', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return only the host name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a FileDraggable when endgameFileName and endgameFilePath are provided, but fileName and filePath are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it prefers to render fileName and filePath over endgameFileName and endgameFilePath respectfully when all of those fields are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { test(`it renders the text "via" when eventAction is ${eventAction}`, () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render the text "via" when eventAction is not a whitelisted action', () => { const eventAction = 'a_non_whitelisted_event_action'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it renders a ParentProcessDraggable when eventAction is NOT "process_stopped" and NOT "termination_event"', () => { const eventAction = 'something_else'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render a ParentProcessDraggable when eventAction is "process_stopped"', () => { const eventAction = 'process_stopped'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ test('it does NOT render a ParentProcessDraggable when eventAction is "termination_event"', () => { const eventAction = 'termination_event'; - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns renders the message when showMessage is true', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it does NOT render the message when showMessage is false', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a ProcessDraggableWithNonExistentProcess when endgamePid and endgameProcessName are provided, but processPid and processName are NOT provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it prefers to render processName and processPid over endgameProcessName and endgamePid respectfully when all of those fields are provided', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('#createGenericSystemRowRenderer', () => { let nonSystem: Ecs; let system: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx index a1121f5f438478..167abe2185bcc5 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx @@ -7,12 +7,14 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { Package } from './package'; describe('Package', () => { + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -54,7 +56,7 @@ describe('Package', () => { }); test('it returns just the package name', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns just the package name and package summary', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns just the package name, package summary, package version', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders against shallow snapshot', () => { const wrapper = shallow( @@ -57,7 +59,7 @@ describe('UserHostWorkingDir', () => { }); test('it returns userDomain if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns "in" + workingDirectory if that is the only attribute defined', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName and workingDirectory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and workingDirectory', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns userName, userDomain, hostName', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and userName with the default hostNameSeparator "@", when hostNameSeparator is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it returns hostName and userName with an overridden hostNameSeparator, when hostNameSeparator is specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable `user.domain` field (by default) when userDomain is provided, and userDomainField is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable with an overridden field name when userDomain is provided, and userDomainField is also specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable `user.name` field (by default) when userName is provided, and userNameField is NOT specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ }); test('it renders a draggable with an overridden field name when userName is provided, and userNameField is also specified as a prop', () => { - const wrapper = mountWithIntl( + const wrapper = mount(
{ + const mount = useMountAppended(); + describe('rendering', () => { test('it renders the default ZeekDetails', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.connection if the data does contain zeek.connection data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.dns if the data does contain zeek.dns data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.http if the data does contain zeek.http data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.notice if the data does contain zeek.notice data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.ssl if the data does contain zeek.ssl data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns zeek.files if the data does contain zeek.files data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { }); test('it returns null for text if the data contains no zeek data', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { + const mount = useMountAppended(); let nonZeek: Ecs; let zeek: Ecs; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index e442f884a8e4b2..4ef2bb89e05ca4 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -8,10 +8,10 @@ import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { Ecs } from '../../../../../graphql/types'; import { mockTimelineData, TestProviders } from '../../../../../mock'; +import { useMountAppended } from '../../../../../utils/use_mount_appended'; import { ZeekSignature, extractStateValue, @@ -27,6 +27,7 @@ import { } from './zeek_signature'; describe('ZeekSignature', () => { + const mount = useMountAppended(); let zeek: Ecs; beforeEach(() => { @@ -70,7 +71,7 @@ describe('ZeekSignature', () => { describe('#TotalVirusLinkSha', () => { test('should return null if value is null', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect( wrapper .find('TotalVirusLinkSha') @@ -80,19 +81,19 @@ describe('ZeekSignature', () => { }); test('should render value', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.text()).toEqual('abc'); }); test('should render link with sha', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.find('a').prop('href')).toEqual('https://www.virustotal.com/#/search/abcdefg'); }); }); describe('#Link', () => { test('should return null if value is null', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect( wrapper .find('Link') @@ -102,12 +103,12 @@ describe('ZeekSignature', () => { }); test('should render value', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.text()).toEqual('abc'); }); test('should render value and link', () => { - const wrapper = mountWithIntl(); + const wrapper = mount(); expect(wrapper.find('a').prop('href')).toEqual( 'https://www.google.com/search?q=somethingelse' ); @@ -116,7 +117,7 @@ describe('ZeekSignature', () => { describe('DraggableZeekElement', () => { test('it returns null if value is null', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -130,7 +131,7 @@ describe('ZeekSignature', () => { }); test('it renders the default ZeekSignature', () => { - const wrapper = mountWithIntl( + const wrapper = mount( @@ -139,7 +140,7 @@ describe('ZeekSignature', () => { }); test('it renders with a custom string renderer', () => { - const wrapper = mountWithIntl( + const wrapper = mount( { describe('#TagTooltip', () => { test('it renders the name of the field in a tooltip', () => { const field = 'zeek.notice'; - const wrapper = mountWithIntl( + const wrapper = mount( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx index 43a7e87a154198..d67c6c9648a151 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { TestProviders } from '../../../mock/test_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { DataProviders } from '.'; import { DataProvider } from './data_provider'; import { mockDataProviders } from './mock/mock_data_providers'; describe('DataProviders', () => { + const mount = useMountAppended(); + describe('rendering', () => { const dropMessage = ['Drop', 'query', 'build', 'here']; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx index d6e092550473dd..c9454846c5548b 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/data_providers/providers.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -15,9 +15,12 @@ import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; import { getDraggableId, Providers } from './providers'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions'; +import { useMountAppended } from '../../../utils/use_mount_appended'; describe('Providers', () => { const mockTimelineContext: boolean = true; + const mount = useMountAppended(); + describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx index 83564fbdc09880..977764803acbb1 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/header/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; @@ -14,6 +14,7 @@ import { mockIndexPattern } from '../../../mock'; import { TestProviders } from '../../../mock/test_providers'; import { mockUiSettings } from '../../../mock/ui_settings'; import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { TimelineHeaderComponent } from '.'; @@ -26,6 +27,7 @@ mockUseKibanaCore.mockImplementation(() => ({ describe('Header', () => { const indexPattern = mockIndexPattern; + const mount = useMountAppended(); describe('rendering', () => { test('renders correctly against snapshot', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx index ebf4ceceafe348..180af88f21e4d7 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/timeline.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import * as React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; @@ -26,6 +26,7 @@ import { import { TimelineComponent } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../utils/use_mount_appended'; const testFlyoutHeight = 980; @@ -50,6 +51,8 @@ describe('Timeline', () => { { request: { query: timelineQuery }, result: { data: { events: mockTimelineData } } }, ]; + const mount = useMountAppended(); + describe('rendering', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( diff --git a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx index 2d9813206bb1e3..8c5a08fdf5e214 100644 --- a/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/truncatable_text/index.test.tsx @@ -10,8 +10,7 @@ import * as React from 'react'; import { TruncatableText } from '.'; -// No style rules found on passed Component -describe.skip('TruncatableText', () => { +describe('TruncatableText', () => { test('renders correctly against snapshot', () => { const wrapper = shallow({'Hiding in plain sight'}); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx index 29e1bc228e066c..e6fec597ed8eae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/create_rule/components/description_step/index.tsx @@ -22,7 +22,6 @@ import { IIndexPattern, esFilters, Query, - utils, } from '../../../../../../../../../../src/plugins/data/public'; import { FilterLabel } from './filter_label'; @@ -126,7 +125,7 @@ const getDescriptionItem = ( diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx index a9a0693a254471..febf9630e968a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount } from 'enzyme'; import React from 'react'; import { IIndexPattern } from 'src/plugins/data/public'; import { MemoryRouter } from 'react-router-dom'; @@ -17,6 +16,7 @@ import { SetAbsoluteRangeDatePicker } from './types'; import { hostDetailsPagePath } from '../types'; import { type } from './utils'; import { useKibanaCore } from '../../../lib/compose/kibana_core'; +import { useMountAppended } from '../../../utils/use_mount_appended'; jest.mock('../../../lib/settings/use_kibana_ui_setting'); @@ -52,6 +52,7 @@ describe('body', () => { anomalies: 'AnomaliesQueryTabBody', events: 'EventsQueryTabBody', }; + const mount = useMountAppended(); Object.entries(scenariosMap).forEach(([path, componentName]) => test(`it should pass expected object properties to ${componentName}`, () => { diff --git a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx index ba3e8a2f375841..72f78588476498 100644 --- a/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/network/ip_details/index.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mount, shallow } from 'enzyme'; +import { shallow } from 'enzyme'; import toJson from 'enzyme-to-json'; import { cloneDeep } from 'lodash/fp'; import * as React from 'react'; @@ -18,6 +18,7 @@ import { mocksSource } from '../../../containers/source/mock'; import { FlowTarget } from '../../../graphql/types'; import { useKibanaCore } from '../../../lib/compose/kibana_core'; import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../mock'; +import { useMountAppended } from '../../../utils/use_mount_appended'; import { mockUiSettings } from '../../../mock/ui_settings'; import { createStore, State } from '../../../store'; import { InputsModelId } from '../../../store/inputs/constants'; @@ -112,6 +113,8 @@ jest.mock('ui/documentation_links', () => ({ })); describe('Ip Details', () => { + const mount = useMountAppended(); + beforeAll(() => { (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => Promise.resolve({ @@ -126,14 +129,15 @@ describe('Ip Details', () => { afterAll(() => { delete (global as GlobalWithFetch).fetch; }); - const state: State = mockGlobalState; + const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); beforeEach(() => { store = createStore(state, apolloClientObservable); localSource = cloneDeep(mocksSource); }); + test('it renders', () => { const wrapper = shallow(); expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); diff --git a/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts b/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts new file mode 100644 index 00000000000000..7b83f77a72023c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/utils/use_mount_appended.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; + +type WrapperOf any> = (...args: Parameters) => ReturnType; // eslint-disable-line +export type MountAppended = WrapperOf; + +export const useMountAppended = () => { + let root: HTMLElement; + + beforeEach(() => { + root = document.createElement('div'); + root.id = 'root'; + document.body.appendChild(root); + }); + + afterEach(() => { + document.body.removeChild(root); + }); + + const mountAppended: MountAppended = (node, options) => + mount(node, { ...options, attachTo: root }); + + return mountAppended; +}; diff --git a/x-pack/legacy/plugins/siem/server/kibana.index.ts b/x-pack/legacy/plugins/siem/server/kibana.index.ts index f56e6b3c3f5502..e90e6366dd9ecd 100644 --- a/x-pack/legacy/plugins/siem/server/kibana.index.ts +++ b/x-pack/legacy/plugins/siem/server/kibana.index.ts @@ -15,9 +15,12 @@ import { findRulesRoute } from './lib/detection_engine/routes/rules/find_rules_r import { deleteRulesRoute } from './lib/detection_engine/routes/rules/delete_rules_route'; import { updateRulesRoute } from './lib/detection_engine/routes/rules/update_rules_route'; import { setSignalsStatusRoute } from './lib/detection_engine/routes/signals/open_close_signals_route'; +import { querySignalsRoute } from './lib/detection_engine/routes/signals/query_signals_route'; import { ServerFacade } from './types'; import { deleteIndexRoute } from './lib/detection_engine/routes/index/delete_index_route'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; +import { readTagsRoute } from './lib/detection_engine/routes/tags/read_tags_route'; +import { readPrivilegesRoute } from './lib/detection_engine/routes/privileges/read_privileges_route'; const APP_ID = 'siem'; @@ -44,10 +47,16 @@ export const initServerWithKibana = (context: PluginInitializerContext, __legacy // POST /api/detection_engine/signals/status // Example usage can be found in siem/server/lib/detection_engine/scripts/signals setSignalsStatusRoute(__legacy); + querySignalsRoute(__legacy); // Detection Engine index routes that have the REST endpoints of /api/detection_engine/index // All REST index creation, policy management for spaces createIndexRoute(__legacy); readIndexRoute(__legacy); deleteIndexRoute(__legacy); + + // Detection Engine tags routes that have the REST endpoints of /api/detection_engine/tags + readTagsRoute(__legacy); + // Privileges API to get the generic user privileges + readPrivilegesRoute(__legacy); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts index 9c8dca0cb370f4..d7cb922b5b6c38 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/create_bootstrap_index.ts @@ -5,7 +5,7 @@ */ import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; // See the reference(s) below on explanations about why -000001 was chosen and // why the is_write_index is true as well as the bootstrapping step which is needed. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts index 6f16eb8fbdeb18..b1d8f994615aea 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_all_index.ts @@ -6,7 +6,7 @@ import { IndicesDeleteParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const deleteAllIndex = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts index 153b9ae4e4136c..92003f165d9962 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const deletePolicy = async ( callWithRequest: CallWithRequest<{ path: string; method: 'DELETE' }, {}, unknown>, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts index b048dd27efb83b..63c32d13ccb8de 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/delete_template.ts @@ -6,7 +6,7 @@ import { IndicesDeleteTemplateParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const deleteTemplate = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts index 24164e894788a4..ff65caa59a866e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.ts @@ -6,7 +6,7 @@ import { IndicesExistsParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const getIndexExists = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts index 847c32d9d61fb3..7541c4217b387e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_policy_exists.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const getPolicyExists = async ( callWithRequest: CallWithRequest<{ path: string; method: 'GET' }, {}, unknown>, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts index 482fc8d855828c..fac402155619e2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_template_exists.ts @@ -6,7 +6,7 @@ import { IndicesExistsTemplateParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const getTemplateExists = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts index 6c9d529078a77d..0abe2b992b7804 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/read_index.ts @@ -6,7 +6,7 @@ import { IndicesGetSettingsParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const readIndex = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts index 2511984b412f34..115f0af75898c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const setPolicy = async ( callWithRequest: CallWithRequest<{ path: string; method: 'PUT'; body: unknown }, {}, unknown>, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts index a679a61e10c001..dc9ad5dda9f7dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/set_template.ts @@ -6,7 +6,7 @@ import { IndicesPutTemplateParams } from 'elasticsearch'; import { CallClusterOptions } from 'src/legacy/core_plugins/elasticsearch'; -import { CallWithRequest } from './types'; +import { CallWithRequest } from '../types'; export const setTemplate = async ( callWithRequest: CallWithRequest, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts new file mode 100644 index 00000000000000..3b84075b9e435a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/privileges/read_privileges.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CallWithRequest } from '../types'; + +export const readPrivileges = async ( + callWithRequest: CallWithRequest, + index: string +): Promise => { + return callWithRequest('transport.request', { + path: `_security/user/_has_privileges`, + method: 'POST', + body: { + cluster: [ + 'all', + 'create_snapshot', + 'manage', + 'manage_api_key', + 'manage_ccr', + 'manage_transform', + 'manage_ilm', + 'manage_index_templates', + 'manage_ingest_pipelines', + 'manage_ml', + 'manage_own_api_key', + 'manage_pipeline', + 'manage_rollup', + 'manage_saml', + 'manage_security', + 'manage_token', + 'manage_watcher', + 'monitor', + 'monitor_transform', + 'monitor_ml', + 'monitor_rollup', + 'monitor_watcher', + 'read_ccr', + 'read_ilm', + 'transport_client', + ], + index: [ + { + names: [index], + privileges: [ + 'all', + 'create', + 'create_doc', + 'create_index', + 'delete', + 'delete_index', + 'index', + 'manage', + 'manage_follow_index', + 'manage_ilm', + 'manage_leader_index', + 'monitor', + 'read', + 'read_cross_cluster', + 'view_index_metadata', + 'write', + ], + }, + ], + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 978434859ef955..d9dd7bb1ff7d0e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -6,10 +6,13 @@ import { ServerInjectOptions } from 'hapi'; import { ActionResult } from '../../../../../../actions/server/types'; -import { SignalsRestParams } from '../../signals/types'; +import { SignalsStatusRestParams, SignalsQueryRestParams } from '../../signals/types'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_PRIVILEGES_URL, + DETECTION_ENGINE_QUERY_SIGNALS_URL, + INTERNAL_RULE_ID_KEY, } from '../../../../../common/constants'; import { RuleAlertType } from '../../rules/types'; import { RuleAlertParamsRest } from '../../types'; @@ -40,17 +43,25 @@ export const typicalPayload = (): Partial> = ], }); -export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ +export const typicalSetStatusSignalByIdsPayload = (): Partial => ({ signal_ids: ['somefakeid1', 'somefakeid2'], status: 'closed', }); -export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ +export const typicalSetStatusSignalByQueryPayload = (): Partial => ({ query: { range: { '@timestamp': { gte: 'now-2M', lte: 'now/M' } } }, status: 'closed', }); -export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ +export const typicalSignalsQuery = (): Partial => ({ + query: { match_all: {} }, +}); + +export const typicalSignalsQueryAggs = (): Partial => ({ + aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, +}); + +export const setStatusSignalMissingIdsAndQueryPayload = (): Partial => ({ status: 'closed', }); @@ -72,6 +83,11 @@ export const getFindRequest = (): ServerInjectOptions => ({ url: `${DETECTION_ENGINE_RULES_URL}/_find`, }); +export const getPrivilegeRequest = (): ServerInjectOptions => ({ + method: 'GET', + url: `${DETECTION_ENGINE_PRIVILEGES_URL}`, +}); + interface FindHit { page: number; perPage: number; @@ -134,6 +150,18 @@ export const getSetSignalStatusByQueryRequest = (): ServerInjectOptions => ({ }, }); +export const getSignalsQueryRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQuery() }, +}); + +export const getSignalsAggsQueryRequest = (): ServerInjectOptions => ({ + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs() }, +}); + export const createActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', @@ -144,7 +172,7 @@ export const createActionResult = (): ActionResult => ({ export const getResult = (): RuleAlertType => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', - tags: [], + tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`], alertTypeId: 'siem.signals', params: { description: 'Detecting root and admin users', @@ -204,3 +232,56 @@ export const updateActionResult = (): ActionResult => ({ name: '', config: {}, }); + +export const getMockPrivileges = () => ({ + username: 'test-space', + has_all_requested: false, + cluster: { + monitor_ml: true, + manage_ccr: false, + manage_index_templates: true, + monitor_watcher: true, + monitor_transform: true, + read_ilm: true, + manage_api_key: false, + manage_security: false, + manage_own_api_key: false, + manage_saml: false, + all: false, + manage_ilm: true, + manage_ingest_pipelines: true, + read_ccr: false, + manage_rollup: true, + monitor: true, + manage_watcher: true, + manage: true, + manage_transform: true, + manage_token: false, + manage_ml: true, + manage_pipeline: true, + monitor_rollup: true, + transport_client: true, + create_snapshot: true, + }, + index: { + '.siem-signals-frank-hassanabad-test-space': { + all: false, + manage_ilm: true, + read: false, + create_index: true, + read_cross_cluster: false, + index: false, + monitor: true, + delete: false, + manage: true, + delete_index: true, + create_doc: false, + view_index_metadata: true, + create: false, + manage_follow_index: true, + manage_leader_index: true, + write: false, + }, + }, + application: {}, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 94c42664c281d0..0eb090179b1925 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -48,7 +48,7 @@ export const createCreateIndexRoute = (server: ServerFacade): Hapi.ServerRoute = const template = getSignalsTemplate(index); await setTemplate(callWithRequest, index, template); } - createBootstrapIndex(callWithRequest, index); + await createBootstrapIndex(callWithRequest, index); return { acknowledged: true }; } } catch (err) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts new file mode 100644 index 00000000000000..1ea681afb79491 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { createMockServer } from '../__mocks__/_mock_server'; +import { getPrivilegeRequest, getMockPrivileges } from '../__mocks__/request_responses'; +import { readPrivilegesRoute } from './read_privileges_route'; +import * as myUtils from '../utils'; + +describe('read_privileges', () => { + let { server, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); + ({ server, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn(() => getMockPrivileges()), + })); + readPrivilegesRoute(server); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('normal status codes', () => { + test('returns 200 when doing a normal request', async () => { + const { statusCode } = await server.inject(getPrivilegeRequest()); + expect(statusCode).toBe(200); + }); + + test('returns the payload when doing a normal request', async () => { + const { payload } = await server.inject(getPrivilegeRequest()); + expect(JSON.parse(payload)).toEqual(getMockPrivileges()); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts new file mode 100644 index 00000000000000..457de05674f661 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants'; +import { RulesRequest } from '../../rules/types'; +import { ServerFacade } from '../../../../types'; +import { callWithRequestFactory, transformError, getIndex } from '../utils'; +import { readPrivileges } from '../../privileges/read_privileges'; + +export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'GET', + path: DETECTION_ENGINE_PRIVILEGES_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RulesRequest) { + try { + const callWithRequest = callWithRequestFactory(request, server); + const index = getIndex(request, server); + const permissions = await readPrivileges(callWithRequest, index); + return permissions; + } catch (err) { + return transformError(err); + } + }, + }; +}; + +export const readPrivilegesRoute = (server: ServerFacade): void => { + server.route(createReadPrivilegesRulesRoute(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index d4e129f543ccfe..a2312ce25e72a9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -11,8 +11,10 @@ import { getIdError, transformFindAlertsOrError, transformOrError, + transformTags, } from './utils'; import { getResult } from '../__mocks__/request_responses'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; describe('utils', () => { describe('transformAlertToRule', () => { @@ -335,6 +337,53 @@ describe('utils', () => { type: 'query', }); }); + + test('should work with tags but filter out any internal tags', () => { + const fullRule = getResult(); + fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; + const rule = transformAlertToRule(fullRule); + expect(rule).toEqual({ + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: ['tag 1', 'tag 2'], + threats: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + techniques: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + to: 'now', + type: 'query', + }); + }); }); describe('getIdError', () => { @@ -493,4 +542,21 @@ describe('utils', () => { expect((output as Boom).message).toEqual('Internal error transforming'); }); }); + + describe('transformTags', () => { + test('it returns tags that have no internal structures', () => { + expect(transformTags(['tag 1', 'tag 2'])).toEqual(['tag 1', 'tag 2']); + }); + + test('it returns empty tags given empty tags', () => { + expect(transformTags([])).toEqual([]); + }); + + test('it returns tags with internal tags stripped out', () => { + expect(transformTags(['tag 1', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 2'])).toEqual([ + 'tag 1', + 'tag 2', + ]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index c9ae3abdfdc6b7..ff06b63a034b89 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { pickBy } from 'lodash/fp'; +import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; import { RuleAlertType, isAlertType, isAlertTypes } from '../../rules/types'; import { OutputRuleAlertRest } from '../../types'; @@ -25,6 +26,10 @@ export const getIdError = ({ } }; +export const transformTags = (tags: string[]): string[] => { + return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); +}; + // Transforms the data but will remove any null or undefined it encounters and not include // those on the export export const transformAlertToRule = (alert: RuleAlertType): Partial => { @@ -51,7 +56,7 @@ export const transformAlertToRule = (alert: RuleAlertType): Partial { + test('query and aggs simultaneously', () => { + expect( + querySignalsSchema.validate>({ + query: {}, + aggs: {}, + }).error + ).toBeFalsy(); + }); + + test('query only', () => { + expect( + querySignalsSchema.validate>({ + query: {}, + }).error + ).toBeFalsy(); + }); + + test('aggs only', () => { + expect( + querySignalsSchema.validate>({ + aggs: {}, + }).error + ).toBeFalsy(); + }); + + test('missing query and aggs is invalid', () => { + expect(querySignalsSchema.validate>({}).error).toBeTruthy(); + }); +}); diff --git a/x-pack/legacy/plugins/apm/public/new-platform/stackVersionFromLegacyMetadata.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts similarity index 65% rename from x-pack/legacy/plugins/apm/public/new-platform/stackVersionFromLegacyMetadata.ts rename to x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts index 3d43b8e39a122f..53ce50692e84aa 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/stackVersionFromLegacyMetadata.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { metadata } from 'ui/metadata'; +import Joi from 'joi'; -export const stackVersionFromLegacyMetadata = metadata.branch; +export const querySignalsSchema = Joi.object({ + query: Joi.object(), + aggs: Joi.object(), +}).min(1); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts index b586b4666bfeef..792c7afad05b19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -5,12 +5,12 @@ */ import { setSignalsStatusSchema } from './set_signal_status_schema'; -import { SignalsRestParams } from '../../signals/types'; +import { SignalsStatusRestParams } from '../../signals/types'; describe('set signal status schema', () => { test('signal_ids and status is valid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ signal_ids: ['somefakeid'], status: 'open', }).error @@ -19,7 +19,7 @@ describe('set signal status schema', () => { test('query and status is valid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ query: {}, status: 'open', }).error @@ -28,7 +28,7 @@ describe('set signal status schema', () => { test('signal_ids and missing status is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ signal_ids: ['somefakeid'], }).error ).toBeTruthy(); @@ -36,7 +36,7 @@ describe('set signal status schema', () => { test('query and missing status is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ query: {}, }).error ).toBeTruthy(); @@ -44,7 +44,7 @@ describe('set signal status schema', () => { test('status is present but query or signal_ids is missing is invalid', () => { expect( - setSignalsStatusSchema.validate>({ + setSignalsStatusSchema.validate>({ status: 'closed', }).error ).toBeTruthy(); @@ -54,7 +54,7 @@ describe('set signal status schema', () => { expect( setSignalsStatusSchema.validate< Partial< - Omit & { + Omit & { status: string; } > diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index b342cc5cd14ef8..7c49a1942ee919 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,7 +6,7 @@ import Hapi from 'hapi'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL } from '../../../../../common/constants'; -import { SignalsRequest } from '../../signals/types'; +import { SignalsStatusRequest } from '../../signals/types'; import { setSignalsStatusSchema } from '../schemas/set_signal_status_schema'; import { ServerFacade } from '../../../../types'; import { transformError, getIndex } from '../utils'; @@ -24,7 +24,7 @@ export const setSignalsStatusRouteDef = (server: ServerFacade): Hapi.ServerRoute payload: setSignalsStatusSchema, }, }, - async handler(request: SignalsRequest, headers) { + async handler(request: SignalsStatusRequest) { const { signal_ids: signalIds, query, status } = request.payload; const index = getIndex(request, server); const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts new file mode 100644 index 00000000000000..1b990e8c1ff572 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -0,0 +1,129 @@ +/* + * 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 { createMockServer } from '../__mocks__/_mock_server'; +import { querySignalsRoute } from './query_signals_route'; +import * as myUtils from '../utils'; +import { ServerInjectOptions } from 'hapi'; +import { + getSignalsQueryRequest, + getSignalsAggsQueryRequest, + typicalSignalsQuery, + typicalSignalsQueryAggs, +} from '../__mocks__/request_responses'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; + +describe('query for signal', () => { + let { server, elasticsearch } = createMockServer(); + + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(myUtils, 'getIndex').mockReturnValue('fakeindex'); + ({ server, elasticsearch } = createMockServer()); + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn(() => true), + })); + querySignalsRoute(server); + }); + + describe('query and agg on signals index', () => { + test('returns 200 when using single query', async () => { + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); + return true; + } + ), + })); + const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 200 when using single agg', async () => { + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ ...typicalSignalsQueryAggs() }); + return true; + } + ), + })); + const { statusCode } = await server.inject(getSignalsAggsQueryRequest()); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 200 when using aggs and query together', async () => { + const allTogether = getSignalsQueryRequest(); + allTogether.payload = { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }; + elasticsearch.getCluster = jest.fn(() => ({ + callWithRequest: jest.fn( + (_req, _type: string, queryBody: { index: string; body: object }) => { + expect(queryBody.body).toMatchObject({ + ...typicalSignalsQueryAggs(), + ...typicalSignalsQuery(), + }); + return true; + } + ), + })); + const { statusCode } = await server.inject(allTogether); + expect(statusCode).toBe(200); + expect(myUtils.getIndex).toHaveReturnedWith('fakeindex'); + }); + + test('returns 400 when missing aggs and query', async () => { + const allTogether = getSignalsQueryRequest(); + allTogether.payload = {}; + const { statusCode } = await server.inject(allTogether); + expect(statusCode).toBe(400); + }); + }); + + describe('validation', () => { + test('returns 200 if query present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: typicalSignalsQuery(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if aggs is present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: typicalSignalsQueryAggs(), + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 200 if aggs and query are present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: { ...typicalSignalsQueryAggs(), ...typicalSignalsQuery() }, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(200); + }); + + test('returns 400 if aggs and query are NOT present', async () => { + const request: ServerInjectOptions = { + method: 'POST', + url: DETECTION_ENGINE_QUERY_SIGNALS_URL, + payload: {}, + }; + const { statusCode } = await server.inject(request); + expect(statusCode).toBe(400); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts new file mode 100644 index 00000000000000..89ffed259cf77a --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -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 Hapi from 'hapi'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../common/constants'; +import { SignalsQueryRequest } from '../../signals/types'; +import { querySignalsSchema } from '../schemas/query_signals_index_schema'; +import { ServerFacade } from '../../../../types'; +import { transformError, getIndex } from '../utils'; + +export const querySignalsRouteDef = (server: ServerFacade): Hapi.ServerRoute => { + return { + method: 'POST', + path: DETECTION_ENGINE_QUERY_SIGNALS_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + payload: querySignalsSchema, + }, + }, + async handler(request: SignalsQueryRequest) { + const { query, aggs } = request.payload; + const body = { query, aggs }; + const index = getIndex(request, server); + const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); + try { + return callWithRequest(request, 'search', { + index, + body, + }); + } catch (exc) { + // error while getting or updating signal with id: id in signal index .siem-signals + return transformError(exc); + } + }, + }; +}; + +export const querySignalsRoute = (server: ServerFacade) => { + server.route(querySignalsRouteDef(server)); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts new file mode 100644 index 00000000000000..beef8b4199c15e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Hapi from 'hapi'; +import { isFunction } from 'lodash/fp'; +import { DETECTION_ENGINE_TAGS_URL } from '../../../../../common/constants'; +import { ServerFacade, RequestFacade } from '../../../../types'; +import { transformError } from '../utils'; +import { readTags } from '../../tags/read_tags'; + +export const createReadTagsRoute: Hapi.ServerRoute = { + method: 'GET', + path: DETECTION_ENGINE_TAGS_URL, + options: { + tags: ['access:siem'], + validate: { + options: { + abortEarly: false, + }, + }, + }, + async handler(request: RequestFacade, headers) { + const alertsClient = isFunction(request.getAlertsClient) ? request.getAlertsClient() : null; + const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; + + if (!alertsClient || !actionsClient) { + return headers.response().code(404); + } + + try { + const tags = await readTags({ + alertsClient, + }); + return tags; + } catch (err) { + return transformError(err); + } + }, +}; + +export const readTagsRoute = (server: ServerFacade) => { + server.route(createReadTagsRoute); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts new file mode 100644 index 00000000000000..5a92c8ef42ed71 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { addRuleIdToTags } from './add_rule_id_to_tags'; +import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; + +describe('add_rule_id_to_tags', () => { + test('it should add a rule id as an internal structure to a single tag', () => { + const tags = addRuleIdToTags(['tag 1'], 'rule-1'); + expect(tags).toEqual(['tag 1', `${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add a rule id as an internal structure to two tags', () => { + const tags = addRuleIdToTags(['tag 1', 'tag 2'], 'rule-1'); + expect(tags).toEqual(['tag 1', 'tag 2', `${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add a rule id as an internal structure with empty tags', () => { + const tags = addRuleIdToTags([], 'rule-1'); + expect(tags).toEqual([`${INTERNAL_RULE_ID_KEY}:rule-1`]); + }); + + test('it should add not add an internal structure if rule id is undefined', () => { + const tags = addRuleIdToTags(['tag 1'], undefined); + expect(tags).toEqual(['tag 1']); + }); + + test('it should add not add an internal structure if rule id is null', () => { + const tags = addRuleIdToTags(['tag 1'], null); + expect(tags).toEqual(['tag 1']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts new file mode 100644 index 00000000000000..1cf97881d514bc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/add_rule_id_to_tags.ts @@ -0,0 +1,15 @@ +/* + * 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 { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; + +export const addRuleIdToTags = (tags: string[], ruleId: string | null | undefined): string[] => { + if (ruleId == null) { + return tags; + } else { + return [...tags, `${INTERNAL_RULE_ID_KEY}:${ruleId}`]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 4cbf3756f58ac1..c4c31190e1e83e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -6,6 +6,7 @@ import { SIGNALS_ID } from '../../../../common/constants'; import { RuleParams } from './types'; +import { addRuleIdToTags } from './add_rule_id_to_tags'; export const createRules = async ({ alertsClient, @@ -37,7 +38,7 @@ export const createRules = async ({ return alertsClient.create({ data: { name, - tags, + tags: addRuleIdToTags(tags, ruleId), alertTypeId: SIGNALS_ID, params: { description, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts index b3d7ab13227750..6ba0aa95bdd7be 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.test.ts @@ -5,14 +5,9 @@ */ import { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; -import { readRules, readRuleByRuleId, findRuleInArrayByRuleId } from './read_rules'; +import { readRules } from './read_rules'; import { AlertsClient } from '../../../../../alerting'; -import { - getResult, - getFindResultWithSingleHit, - getFindResultWithMultiHits, -} from '../routes/__mocks__/request_responses'; -import { SIGNALS_ID } from '../../../../common/constants'; +import { getResult, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; describe('read_rules', () => { describe('readRules', () => { @@ -98,141 +93,4 @@ describe('read_rules', () => { expect(rule).toEqual(null); }); }); - - describe('readRuleByRuleId', () => { - test('should return a single value if the rule id matches', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(rule).toEqual(getResult()); - }); - - test('should not return a single value if the rule id does not match', async () => { - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(rule).toEqual(null); - }); - - test('should return a single value of rule-1 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-1', - }); - expect(rule).toEqual(result1); - }); - - test('should return a single value of rule-2 with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-2', - }); - expect(rule).toEqual(result2); - }); - - test('should return null for a made up value with multiple values', async () => { - const result1 = getResult(); - result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result1.params.ruleId = 'rule-1'; - - const result2 = getResult(); - result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; - result2.params.ruleId = 'rule-2'; - - const alertsClient = alertsClientMock.create(); - alertsClient.get.mockResolvedValue(getResult()); - alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); - - const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; - const rule = await readRuleByRuleId({ - alertsClient: unsafeCast, - ruleId: 'rule-that-should-not-match-anything', - }); - expect(rule).toEqual(null); - }); - }); - - describe('findRuleInArrayByRuleId', () => { - test('returns null if the objects are not of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: 'made up 1', params: { ruleId: '123' } }, - { alertTypeId: 'made up 2', params: { ruleId: '456' } }, - ], - '123' - ); - expect(rule).toEqual(null); - }); - - test('returns correct type if the objects are of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: 'made up 2', params: { ruleId: '456' } }, - ], - '123' - ); - expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '123' } }); - }); - - test('returns second correct type if the objects are of a signal rule type', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, - ], - '456' - ); - expect(rule).toEqual({ alertTypeId: 'siem.signals', params: { ruleId: '456' } }); - }); - - test('returns null with correct types but data does not exist', () => { - const rule = findRuleInArrayByRuleId( - [ - { alertTypeId: SIGNALS_ID, params: { ruleId: '123' } }, - { alertTypeId: SIGNALS_ID, params: { ruleId: '456' } }, - ], - '892' - ); - expect(rule).toEqual(null); - }); - }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts index 5c335263290163..9c83ae924486d4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/read_rules.ts @@ -4,66 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ +import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; -import { RuleAlertType, isAlertTypeArray, ReadRuleParams, ReadRuleByRuleId } from './types'; +import { RuleAlertType, ReadRuleParams, isAlertType } from './types'; -export const findRuleInArrayByRuleId = ( - objects: object[], - ruleId: string -): RuleAlertType | null => { - if (isAlertTypeArray(objects)) { - const rules: RuleAlertType[] = objects; - const rule: RuleAlertType[] = rules.filter(datum => { - return datum.params.ruleId === ruleId; - }); - if (rule.length !== 0) { - return rule[0]; - } else { - return null; - } - } else { - return null; - } -}; - -// This an extremely slow and inefficient way of getting a rule by its id. -// I have to manually query every single record since the rule Params are -// not indexed and I cannot push in my own _id when I create an alert at the moment. -// TODO: Once we can directly push in the _id, then we should no longer need this way. -// TODO: This is meant to be _very_ temporary. -export const readRuleByRuleId = async ({ +/** + * This reads the rules through a cascade try of what is fastest to what is slowest. + * @param id - This is the fastest. This is the auto-generated id through the parameter id. + * and the id will either be found through `alertsClient.get({ id })` or it will not + * be returned as a not-found or a thrown error that is not 404. + * @param ruleId - This is a close second to being fast as long as it can find the rule_id from + * a filter query against the tags using `alert.attributes.tags: "__internal:${ruleId}"]` + */ +export const readRules = async ({ alertsClient, + id, ruleId, -}: ReadRuleByRuleId): Promise => { - const firstRules = await findRules({ alertsClient, page: 1 }); - const firstRule = findRuleInArrayByRuleId(firstRules.data, ruleId); - if (firstRule != null) { - return firstRule; - } else { - const totalPages = Math.ceil(firstRules.total / firstRules.perPage); - return Array(totalPages) - .fill({}) - .map((_, page) => { - // page index never starts at zero. It always has to be 1 or greater - return findRules({ alertsClient, page: page + 1 }); - }) - .reduce>(async (accum, findRule) => { - const rules = await findRule; - const rule = findRuleInArrayByRuleId(rules.data, ruleId); - if (rule != null) { - return rule; - } else { - return accum; - } - }, Promise.resolve(null)); - } -}; - -export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => { +}: ReadRuleParams): Promise => { if (id != null) { try { - const output = await alertsClient.get({ id }); - return output; + const rule = await alertsClient.get({ id }); + if (isAlertType(rule)) { + return rule; + } else { + return null; + } } catch (err) { if (err.output.statusCode === 404) { return null; @@ -73,7 +38,16 @@ export const readRules = async ({ alertsClient, id, ruleId }: ReadRuleParams) => } } } else if (ruleId != null) { - return readRuleByRuleId({ alertsClient, ruleId }); + const ruleFromFind = await findRules({ + alertsClient, + filter: `alert.attributes.tags: "${INTERNAL_RULE_ID_KEY}:${ruleId}"`, + page: 1, + }); + if (ruleFromFind.data.length === 0 || !isAlertType(ruleFromFind.data[0])) { + return null; + } else { + return ruleFromFind.data[0]; + } } else { // should never get here, and yet here we are. return null; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 5c0fa76b52620d..caeec68c504e69 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -84,11 +84,6 @@ export interface ReadRuleParams { ruleId?: string | undefined | null; } -export interface ReadRuleByRuleId { - alertsClient: AlertsClient; - ruleId: string; -} - export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { return obj.every(rule => isAlertType(rule)); }; @@ -96,7 +91,3 @@ export const isAlertTypes = (obj: unknown[]): obj is RuleAlertType[] => { export const isAlertType = (obj: unknown): obj is RuleAlertType => { return get('alertTypeId', obj) === SIGNALS_ID; }; - -export const isAlertTypeArray = (objArray: unknown[]): objArray is RuleAlertType[] => { - return objArray.length === 0 || isAlertType(objArray[0]); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 2eaa05ae2fa6ae..b37e9f6cb8538f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -8,6 +8,7 @@ import { defaults } from 'lodash/fp'; import { AlertAction } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; import { UpdateRuleParams } from './types'; +import { updateTags } from './update_tags'; export const calculateInterval = ( interval: string | undefined, @@ -114,7 +115,7 @@ export const updateRules = async ({ return alertsClient.update({ id: rule.id, data: { - tags: tags != null ? tags : [], + tags: updateTags(rule.tags, tags), name: calculateName({ updatedName: name, originalName: rule.name }), interval: calculateInterval(interval, rule.interval), actions, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts new file mode 100644 index 00000000000000..cca937d73bd74f --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.test.ts @@ -0,0 +1,35 @@ +/* + * 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 { updateTags } from './update_tags'; +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; + +describe('update_tags', () => { + test('it should copy internal structures but not any other tags when updating', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], ['tag 1']); + expect(tags).toEqual([`${INTERNAL_IDENTIFIER}_some_value`, 'tag 1']); + }); + + test('it should copy internal structures but not any other tags if given an update of empty tags', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], []); + expect(tags).toEqual([`${INTERNAL_IDENTIFIER}_some_value`]); + }); + + test('it should work like a normal update if there are no internal structures', () => { + const tags = updateTags(['tag 2', 'tag 3'], ['tag 1']); + expect(tags).toEqual(['tag 1']); + }); + + test('it should not perform an update if the nextTags are undefined', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], undefined); + expect(tags).toEqual(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3']); + }); + + test('it should not perform an update if the nextTags are null', () => { + const tags = updateTags(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3'], null); + expect(tags).toEqual(['tag 2', `${INTERNAL_IDENTIFIER}_some_value`, 'tag 3']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts new file mode 100644 index 00000000000000..cf1424ea31600c --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_tags.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; + +export const updateTags = (prevTags: string[], nextTags: string[] | undefined | null): string[] => { + if (nextTags == null) { + return prevTags; + } else { + const allInternalStructures = prevTags.filter(tag => tag.startsWith(INTERNAL_IDENTIFIER)); + return [...allInternalStructures, ...nextTags]; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh new file mode 100755 index 00000000000000..f82a0b6b34abf8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_privileges.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_privileges.sh +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/privileges | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh new file mode 100755 index 00000000000000..2458c6aeba2485 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/get_tags.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./get_tags.sh +curl -s -k \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X GET ${KIBANA_URL}${SPACE_URL}/api/detection_engine/tags | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh index 53e7bb504746d9..0dd4d85ea9da84 100755 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/post_x_rules.sh @@ -32,6 +32,7 @@ do { \"type\": \"query\", \"from\": \"now-6m\", \"to\": \"now\", + \"enabled\": \"false\", \"query\": \"user.name: root or user.name: admin\", \"language\": \"kuery\" }" \ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh new file mode 100755 index 00000000000000..27186a14af902b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/aggs_signals.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./signals/aggs_signal.sh + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/search \ + -d '{"aggs": {"statuses": {"terms": {"field": "signal.status", "size": 10 }}}}' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh new file mode 100755 index 00000000000000..2fc76406ec0f60 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/signals/query_signals.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +# +# 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. +# + +set -e +./check_env_variables.sh + +# Example: ./signals/query_signals.sh + curl -s -k \ + -H 'Content-Type: application/json' \ + -H 'kbn-xsrf: 123' \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -X POST ${KIBANA_URL}${SPACE_URL}/api/detection_engine/signals/search \ + -d '{ "query": { "match_all": {} } } ' \ + | jq . diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 213ceb29a6e256..a30182c5378843 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -15,12 +15,29 @@ export interface SignalsParams { status: 'open' | 'closed'; } -export type SignalsRestParams = Omit & { - signal_ids: SignalsParams['signalIds']; +export interface SignalsStatusParams { + signalIds: string[] | undefined | null; + query: object | undefined | null; + status: 'open' | 'closed'; +} + +export interface SignalQueryParams { + query: object | undefined | null; + aggs: object | undefined | null; +} + +export type SignalsStatusRestParams = Omit & { + signal_ids: SignalsStatusParams['signalIds']; }; -export interface SignalsRequest extends RequestFacade { - payload: SignalsRestParams; +export type SignalsQueryRestParams = SignalQueryParams; + +export interface SignalsStatusRequest extends RequestFacade { + payload: SignalsStatusRestParams; +} + +export interface SignalsQueryRequest extends RequestFacade { + payload: SignalsQueryRestParams; } export type SearchTypes = diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts new file mode 100644 index 00000000000000..2d562672a4a638 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.test.ts @@ -0,0 +1,364 @@ +/* + * 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 { alertsClientMock } from '../../../../../alerting/server/alerts_client.mock'; +import { AlertsClient } from '../../../../../alerting'; +import { getResult, getFindResultWithMultiHits } from '../routes/__mocks__/request_responses'; +import { INTERNAL_RULE_ID_KEY, INTERNAL_IDENTIFIER } from '../../../../common/constants'; +import { readRawTags, readTags, convertTagsToSet, convertToTags, isTags } from './read_tags'; + +describe('read_tags', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('readRawTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readRawTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + }); + + describe('readTags', () => { + test('it should return the intersection of tags to where none are repeating', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should return the intersection of tags to where some are repeating values', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should work with no tags defined between two results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1, result2])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should work with a single tag which has repeating values in it', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 1', 'tag 1', 'tag 2']; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2']); + }); + + test('it should work with a single tag which has empty tags', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = []; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual([]); + }); + + test('it should filter out any __internal tags for things such as alert_id', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1']); + }); + + test('it should filter out any __internal tags with two different results', async () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + 'tag 5', + ]; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = [ + `${INTERNAL_IDENTIFIER}_some_value`, + `${INTERNAL_RULE_ID_KEY}_some_value`, + 'tag 1', + 'tag 2', + 'tag 3', + 'tag 4', + ]; + + const alertsClient = alertsClientMock.create(); + alertsClient.find.mockResolvedValue(getFindResultWithMultiHits([result1])); + + const unsafeCast: AlertsClient = (alertsClient as unknown) as AlertsClient; + const tags = await readTags({ + alertsClient: unsafeCast, + }); + expect(tags).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4', 'tag 5']); + }); + }); + + describe('convertTagsToSet', () => { + test('it should convert the intersection of two tag systems without duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2]); + const set = convertTagsToSet(findResult.data); + expect(Array.from(set)).toEqual(['tag 1', 'tag 2', 'tag 3', 'tag 4']); + }); + + test('it should with with an empty array', () => { + const set = convertTagsToSet([]); + expect(Array.from(set)).toEqual([]); + }); + }); + + describe('convertToTags', () => { + test('it should convert the two tag systems together with duplicates', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result2.params.ruleId = 'rule-2'; + result2.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2]); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should filter out anything that is not a tag', () => { + const result1 = getResult(); + result1.id = '4baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result1.params.ruleId = 'rule-1'; + result1.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3']; + + const result2 = getResult(); + result2.id = '99979e67-19a7-455f-b452-8eded6135716'; + result2.params.ruleId = 'rule-2'; + delete result2.tags; + + const result3 = getResult(); + result3.id = '5baa53f8-96da-44ee-ad58-41bccb7f9f3d'; + result3.params.ruleId = 'rule-2'; + result3.tags = ['tag 1', 'tag 2', 'tag 2', 'tag 3', 'tag 4']; + + const findResult = getFindResultWithMultiHits([result1, result2, result3]); + const tags = convertToTags(findResult.data); + expect(tags).toEqual([ + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 1', + 'tag 2', + 'tag 2', + 'tag 3', + 'tag 4', + ]); + }); + + test('it should with with an empty array', () => { + const tags = convertToTags([]); + expect(tags).toEqual([]); + }); + }); + + describe('isTags', () => { + test('it should return true if the object has a tags on it', () => { + expect(isTags({ tags: [] })).toBe(true); + }); + + test('it should return false if the object does not have a tags on it', () => { + expect(isTags({})).toBe(false); + }); + }); +}); 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 new file mode 100644 index 00000000000000..0f973d816917fc --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/tags/read_tags.ts @@ -0,0 +1,80 @@ +/* + * 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 { has } from 'lodash/fp'; +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[]; +} + +export const isTags = (obj: object): obj is TagType => { + return has('tags', obj); +}; + +export const convertToTags = (tagObjects: object[]): string[] => { + const tags = tagObjects.reduce((accum, tagObj) => { + if (isTags(tagObj)) { + return [...accum, ...tagObj.tags]; + } else { + return accum; + } + }, []); + return tags; +}; + +export const convertTagsToSet = (tagObjects: object[]): Set => { + return new Set(convertToTags(tagObjects)); +}; + +// Note: This is doing an in-memory aggregation of the tags by calling each of the alerting +// records in batches of this const setting and uses the fields to try to get the least +// amount of data per record back. If saved objects at some point supports aggregations +// then this should be replaced with a an aggregation call. +// 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 => { + const tags = await readRawTags({ alertsClient, perPage }); + return tags.filter(tag => !tag.startsWith(INTERNAL_IDENTIFIER)); +}; + +export const readRawTags = async ({ + alertsClient, + perPage = DEFAULT_PER_PAGE, +}: { + alertsClient: AlertsClient; + perPage?: number; +}): Promise => { + 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>>(async (accum, nextTagPage) => { + const tagArray = convertToTags((await nextTagPage).data); + return new Set([...(await accum), ...tagArray]); + }, Promise.resolve(firstSet)); + + return Array.from(returnTags); + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index d02595c368aa75..bb616554042f4f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -65,3 +65,5 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { created_by: string | undefined | null; updated_by: string | undefined | null; }; + +export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 533b6c23088eca..25fa3bd6cde4cb 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -45,17 +45,24 @@ export class Plugin { catalogue: ['siem'], privileges: { all: { - api: ['siem'], + api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], savedObject: { - all: [noteSavedObjectType, pinnedEventSavedObjectType, timelineSavedObjectType], + all: [ + 'alert', + 'action', + 'action_task_params', + noteSavedObjectType, + pinnedEventSavedObjectType, + timelineSavedObjectType, + ], read: ['config'], }, ui: ['show'], }, read: { - api: ['siem'], + api: ['siem', 'actions-read', 'actions-all', 'alerting-read', 'alerting-all'], savedObject: { - all: [], + all: ['alert', 'action', 'action_task_params'], read: [ 'config', noteSavedObjectType, diff --git a/x-pack/legacy/plugins/snapshot_restore/index.ts b/x-pack/legacy/plugins/snapshot_restore/index.ts index 0cc1043e25557b..19b67b41be2a68 100644 --- a/x-pack/legacy/plugins/snapshot_restore/index.ts +++ b/x-pack/legacy/plugins/snapshot_restore/index.ts @@ -7,8 +7,8 @@ import { Legacy } from 'kibana'; import { resolve } from 'path'; import { PLUGIN } from './common/constants'; -import { Plugin as SnapshotRestorePlugin } from './plugin'; -import { createShim } from './shim'; +import { Plugin as SnapshotRestorePlugin } from './server/plugin'; +import { createShim } from './server/shim'; export function snapshotRestore(kibana: any) { return new kibana.Plugin({ diff --git a/x-pack/legacy/plugins/snapshot_restore/plugin.ts b/x-pack/legacy/plugins/snapshot_restore/server/plugin.ts similarity index 79% rename from x-pack/legacy/plugins/snapshot_restore/plugin.ts rename to x-pack/legacy/plugins/snapshot_restore/server/plugin.ts index 35ef05f91be8e5..f9264ee1f25077 100644 --- a/x-pack/legacy/plugins/snapshot_restore/plugin.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/plugin.ts @@ -3,8 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { API_BASE_PATH } from './common/constants'; -import { registerRoutes } from './server/routes/api/register_routes'; +import { API_BASE_PATH } from '../common/constants'; +import { registerRoutes } from './routes/api/register_routes'; import { Core, Plugins } from './shim'; export class Plugin { diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts index 6c7ad0ae303875..9961801ecc6c78 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/app.ts @@ -13,7 +13,7 @@ import { // NOTE: now we import it from our "public" folder, but when the Authorisation lib // will move to the "es_ui_shared" plugin, it will be imported from its "static" folder import { Privileges } from '../../../public/app/lib/authorization'; -import { Plugins } from '../../../shim'; +import { Plugins } from '../../shim'; let xpackMainPlugin: any; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts index 38f9a2301af5a3..bbfc82b8a6de9a 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/policy.ts @@ -10,7 +10,7 @@ import { } from '../../../../../server/lib/create_router/error_wrappers'; import { SlmPolicyEs, SlmPolicy, SlmPolicyPayload } from '../../../common/types'; import { deserializePolicy, serializePolicy } from '../../../common/lib'; -import { Plugins } from '../../../shim'; +import { Plugins } from '../../shim'; import { getManagedPolicyNames } from '../../lib'; let callWithInternalUser: any; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts index 11a6cad86640e4..713df194044d33 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/register_routes.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { Router } from '../../../../../server/lib/create_router'; -import { Plugins } from '../../../shim'; +import { Plugins } from '../../shim'; import { registerAppRoutes } from './app'; import { registerRepositoriesRoutes } from './repositories'; import { registerSnapshotsRoutes } from './snapshots'; diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts index c394bafeef1f0f..324c03d9c84c35 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -19,7 +19,7 @@ import { RepositoryCleanup, } from '../../../common/types'; -import { Plugins } from '../../../shim'; +import { Plugins } from '../../shim'; import { deserializeRepositorySettings, serializeRepositorySettings, diff --git a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts index eed47b7343ec5b..042a2dfeaf6b53 100644 --- a/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -10,7 +10,7 @@ import { } from '../../../../../server/lib/create_router/error_wrappers'; import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; import { deserializeSnapshotDetails } from '../../../common/lib'; -import { Plugins } from '../../../shim'; +import { Plugins } from '../../shim'; import { getManagedRepositoryName } from '../../lib'; let callWithInternalUser: any; diff --git a/x-pack/legacy/plugins/snapshot_restore/shim.ts b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts similarity index 83% rename from x-pack/legacy/plugins/snapshot_restore/shim.ts rename to x-pack/legacy/plugins/snapshot_restore/server/shim.ts index ef8d65fca77d46..84c9ddf8e0bea6 100644 --- a/x-pack/legacy/plugins/snapshot_restore/shim.ts +++ b/x-pack/legacy/plugins/snapshot_restore/server/shim.ts @@ -6,10 +6,10 @@ import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; -import { createRouter, Router } from '../../server/lib/create_router'; -import { registerLicenseChecker } from '../../server/lib/register_license_checker'; -import { elasticsearchJsPlugin } from './server/client/elasticsearch_slm'; -import { CloudSetup } from '../../../plugins/cloud/server'; +import { createRouter, Router } from '../../../server/lib/create_router'; +import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; +import { elasticsearchJsPlugin } from './client/elasticsearch_slm'; +import { CloudSetup } from '../../../../plugins/cloud/server'; export interface Core { http: { createRouter(basePath: string): Router; diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 6ea06f47b90120..b20ddacc7e527c 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -8,7 +8,7 @@ import { resolve } from 'path'; import KbnServer, { Server } from 'src/legacy/server/kbn_server'; import { Legacy } from 'kibana'; import { KibanaRequest } from '../../../../src/core/server'; -import { SpacesServiceSetup } from '../../../plugins/spaces/server/spaces_service/spaces_service'; +import { SpacesServiceSetup } from '../../../plugins/spaces/server'; import { SpacesPluginSetup } from '../../../plugins/spaces/server'; // @ts-ignore import { AuditLogger } from '../../server/lib/audit_logger'; diff --git a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts index d2576ca5c6c16b..9fcc5a89736cc3 100644 --- a/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts +++ b/x-pack/legacy/plugins/spaces/public/lib/copy_saved_objects_to_space/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/server'; +import { SavedObjectsImportRetry, SavedObjectsImportResponse } from 'src/core/public'; export interface CopyOptions { includeRelated: boolean; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx index 4485491f5cd892..a69a8f47263e6d 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx @@ -7,7 +7,7 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../plugins/features/public'; import { Space } from '../../../../../common/model/space'; import { SectionPanel } from '../section_panel'; import { EnabledFeatures } from './enabled_features'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx index 1c1925a6a4ee0b..b1f3e8c43de9c9 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; import { UICapabilities } from 'ui/capabilities'; -import { Feature } from '../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../plugins/features/public'; import { Space } from '../../../../../common/model/space'; import { getEnabledFeatures } from '../../lib/feature_utils'; import { SectionPanel } from '../section_panel'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx index 91f14cf228c552..d408ea6582742f 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx @@ -8,7 +8,7 @@ import { EuiCheckbox, EuiIcon, EuiInMemoryTable, EuiSwitch, EuiText, IconType } import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component } from 'react'; -import { Feature } from '../../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../../plugins/features/public'; import { Space } from '../../../../../common/model/space'; import { ToggleAllFeatures } from './toggle_all_features'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx index 7a3fea0d76a3b9..dfd60f7c193c1c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/edit_space/manage_space_page.tsx @@ -22,7 +22,7 @@ import { capabilities } from 'ui/capabilities'; import { Breadcrumb } from 'ui/chrome'; import { kfetch } from 'ui/kfetch'; import { toastNotifications } from 'ui/notify'; -import { Feature } from '../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../plugins/features/public'; import { isReservedSpace } from '../../../../common'; import { Space } from '../../../../common/model/space'; import { SpacesManager } from '../../../lib'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts b/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts index 3420a4ccd7278d..8621ec5614368c 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts +++ b/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../plugins/features/public'; import { getEnabledFeatures } from './feature_utils'; const buildFeatures = () => diff --git a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts b/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts index 0ff428c7117841..ef46a539677442 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts +++ b/x-pack/legacy/plugins/spaces/public/views/management/lib/feature_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../plugins/features/public'; import { Space } from '../../../../common/model/space'; diff --git a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx index bd7c61a018c9f7..c6ad2e36740d95 100644 --- a/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/legacy/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx @@ -23,7 +23,7 @@ import { capabilities } from 'ui/capabilities'; import { kfetch } from 'ui/kfetch'; // @ts-ignore import { toastNotifications } from 'ui/notify'; -import { Feature } from '../../../../../../../plugins/features/server'; +import { Feature } from '../../../../../../../plugins/features/public'; import { isReservedSpace } from '../../../../common'; import { DEFAULT_SPACE_ID } from '../../../../common/constants'; import { Space } from '../../../../common/model/space'; diff --git a/x-pack/legacy/plugins/task_manager/migrations.ts b/x-pack/legacy/plugins/task_manager/migrations.ts index dd6651fddb90a6..65ca38d0b447dd 100644 --- a/x-pack/legacy/plugins/task_manager/migrations.ts +++ b/x-pack/legacy/plugins/task_manager/migrations.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +// Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should +// be moved under task_manager/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObject } from 'src/core/server'; export const migrations = { diff --git a/x-pack/legacy/plugins/task_manager/task_manager.test.ts b/x-pack/legacy/plugins/task_manager/task_manager.test.ts index 0b4a22910e6111..b95a8a47f40954 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.test.ts @@ -7,7 +7,11 @@ import _ from 'lodash'; import sinon from 'sinon'; import { TaskManager, claimAvailableTasks } from './task_manager'; +// Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should +// be moved under task_manager/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { savedObjectsClientMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsSerializer, SavedObjectsSchema } from 'src/core/server'; import { mockLogger } from './test_utils'; diff --git a/x-pack/legacy/plugins/task_manager/task_manager.ts b/x-pack/legacy/plugins/task_manager/task_manager.ts index 269d7ff67384b4..6622a84b31fbb1 100644 --- a/x-pack/legacy/plugins/task_manager/task_manager.ts +++ b/x-pack/legacy/plugins/task_manager/task_manager.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { performance } from 'perf_hooks'; +// Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should +// be moved under task_manager/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsClientContract, SavedObjectsSerializer } from 'src/core/server'; import { Logger } from './types'; import { fillPool, FillPoolResult } from './lib/fill_pool'; diff --git a/x-pack/legacy/plugins/task_manager/task_runner.test.ts b/x-pack/legacy/plugins/task_manager/task_runner.test.ts index 578b86ba0b3f69..72d4be736955b9 100644 --- a/x-pack/legacy/plugins/task_manager/task_runner.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_runner.test.ts @@ -10,7 +10,10 @@ import { minutesFromNow } from './lib/intervals'; import { ConcreteTaskInstance } from './task'; import { TaskManagerRunner } from './task_runner'; import { mockLogger } from './test_utils'; -import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; +// Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should +// be moved under task_manager/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; let fakeTimer: sinon.SinonFakeTimers; diff --git a/x-pack/legacy/plugins/task_manager/task_store.test.ts b/x-pack/legacy/plugins/task_manager/task_store.test.ts index 46efc4bb57ba76..eb58a9f797943e 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.test.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.test.ts @@ -9,7 +9,11 @@ import sinon from 'sinon'; import uuid from 'uuid'; import { TaskDictionary, TaskDefinition, TaskInstance, TaskStatus } from './task'; import { FetchOpts, StoreOpts, OwnershipClaimingOpts, TaskStore } from './task_store'; +// Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should +// be moved under task_manager/server/ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { savedObjectsClientMock } from 'src/core/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectsSerializer, SavedObjectsSchema, SavedObjectAttributes } from 'src/core/server'; const taskDefinitions: TaskDictionary = { diff --git a/x-pack/legacy/plugins/task_manager/task_store.ts b/x-pack/legacy/plugins/task_manager/task_store.ts index 58bffd2269eb6f..919ef43abd6e2c 100644 --- a/x-pack/legacy/plugins/task_manager/task_store.ts +++ b/x-pack/legacy/plugins/task_manager/task_store.ts @@ -15,6 +15,9 @@ import { SavedObjectAttributes, SavedObjectsSerializer, SavedObjectsRawDoc, + // Task manager uses an unconventional directory structure so the linter marks this as a violation, server files should + // be moved under task_manager/server/ + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from 'src/core/server'; import { ConcreteTaskInstance, diff --git a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts b/x-pack/legacy/plugins/upgrade_assistant/common/types.ts index ce653e461e13b8..0e65506bb584d8 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/common/types.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/common/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject, SavedObjectAttributes } from 'src/core/server'; +import { SavedObject, SavedObjectAttributes } from 'src/core/public'; export enum ReindexStep { // Enum values are spaced out by 10 to give us room to insert steps in between. diff --git a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts index edee1d09cdeeb7..77ba97529c32f4 100644 --- a/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts +++ b/x-pack/legacy/plugins/upgrade_assistant/server/np_ready/types.ts @@ -6,7 +6,7 @@ import { Legacy } from 'kibana'; import { IRouter } from 'src/core/server'; import { ElasticsearchPlugin } from 'src/legacy/core_plugins/elasticsearch'; -import { XPackMainPlugin } from '../../../xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; export interface ServerShim { plugins: { diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/common.ts new file mode 100644 index 00000000000000..84e3ae33294f06 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/common.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. + */ + +import * as t from 'io-ts'; + +export const LocationType = t.partial({ + lat: t.string, + lon: t.string, +}); + +export const CheckGeoType = t.partial({ + name: t.string, + location: LocationType, +}); + +export const SummaryType = t.partial({ + up: t.number, + down: t.number, + geo: CheckGeoType, +}); + +export type Summary = t.TypeOf; +export type CheckGeo = t.TypeOf; +export type Location = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts index a88e28f2e5a095..224892eb917839 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/index.ts @@ -4,5 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export * from './common'; export * from './snapshot'; export * from './monitor/monitor_details'; +export * from './monitor/monitor_locations'; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_locations.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_locations.ts new file mode 100644 index 00000000000000..a40453b3671b79 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/monitor/monitor_locations.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 * as t from 'io-ts'; +import { CheckGeoType, SummaryType } from '../common'; + +// IO type for validation +export const MonitorLocationType = t.partial({ + summary: SummaryType, + geo: CheckGeoType, +}); + +// Typescript type for type checking +export type MonitorLocation = t.TypeOf; + +export const MonitorLocationsType = t.intersection([ + t.type({ monitorId: t.string }), + t.partial({ locations: t.array(MonitorLocationType) }), +]); +export type MonitorLocations = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/apps/index.ts b/x-pack/legacy/plugins/uptime/public/apps/index.ts index 53a74022778f4c..06776842aa6ded 100644 --- a/x-pack/legacy/plugins/uptime/public/apps/index.ts +++ b/x-pack/legacy/plugins/uptime/public/apps/index.ts @@ -7,6 +7,7 @@ import chrome from 'ui/chrome'; import { npStart } from 'ui/new_platform'; import { Plugin } from './plugin'; +import 'uiExports/embeddableFactories'; new Plugin( { opaqueId: Symbol('uptime'), env: {} as any, config: { get: () => ({} as any) } }, 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/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap index 204dcdbe5b5166..4fda42e510bbad 100644 --- 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/__tests__/__snapshots__/monitor_status.bar.test.tsx.snap @@ -2,71 +2,67 @@ exports[`MonitorStatusBar component renders duration in ms, not us 1`] = `
-
- -
-
- Up -
+
-
-
-
+
+
- 1234ms -
-
+
+ 1234ms +
+
+ 15 minutes ago +
`; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts new file mode 100644 index 00000000000000..9b902651690bf0 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/__mocks__/mock.ts @@ -0,0 +1,185 @@ +/* + * 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 lowPolyLayerFeatures from '../low_poly_layer.json'; + +export const mockDownPointsLayer = { + id: 'down_points', + label: 'Down Locations', + sourceDescriptor: { + type: 'GEOJSON_FILE', + __featureCollection: { + features: [ + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [13.399262, 52.487239], + }, + }, + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [13.399262, 55.487239], + }, + }, + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [14.399262, 54.487239], + }, + }, + ], + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#BC261E', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', +}; + +export const mockUpPointsLayer = { + id: 'up_points', + label: 'Up Locations', + sourceDescriptor: { + type: 'GEOJSON_FILE', + __featureCollection: { + features: [ + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [13.399262, 52.487239], + }, + }, + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [13.399262, 55.487239], + }, + }, + { + type: 'feature', + geometry: { + type: 'Point', + coordinates: [14.399262, 54.487239], + }, + }, + ], + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', +}; + +export const mockLayerList = [ + { + id: 'low_poly_layer', + label: 'World countries', + minZoom: 0, + maxZoom: 24, + alpha: 1, + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', + type: 'GEOJSON_FILE', + __featureCollection: lowPolyLayerFeatures, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#cad3e4', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 0, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }, + mockDownPointsLayer, + mockUpPointsLayer, +]; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx new file mode 100644 index 00000000000000..a5578d9e056671 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -0,0 +1,102 @@ +/* + * 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, useState } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; + +import { start } from '../../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import * as i18n from './translations'; +// @ts-ignore +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../maps/common/constants'; + +import { MapEmbeddable } from './types'; +import { getLayerList } from './map_config'; + +export interface EmbeddedMapProps { + upPoints: LocationPoint[]; + downPoints: LocationPoint[]; +} + +export interface LocationPoint { + lat: string; + lon: string; +} + +const EmbeddedPanel = styled.div` + z-index: auto; + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + .embPanel__content { + display: flex; + flex: 1 1 100%; + z-index: 1; + min-height: 0; // Absolute must for Firefox to scroll contents + } + &&& .mapboxgl-canvas { + animation: none !important; + } +`; + +export const EmbeddedMap = ({ upPoints, downPoints }: EmbeddedMapProps) => { + const [embeddable, setEmbeddable] = useState(); + + useEffect(() => { + async function setupEmbeddable() { + const mapState = { + layerList: getLayerList(upPoints, downPoints), + title: i18n.MAP_TITLE, + }; + // @ts-ignore + const embeddableObject = await factory.createFromState(mapState, input, undefined); + + setEmbeddable(embeddableObject); + } + setupEmbeddable(); + }, []); + + useEffect(() => { + if (embeddable) { + embeddable.setLayerList(getLayerList(upPoints, downPoints)); + } + }, [upPoints, downPoints]); + + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + }, [embeddable]); + + const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + + const input = { + id: uuid.v4(), + filters: [], + hidePanelTitles: true, + query: { query: '', language: 'kuery' }, + refreshConfig: { value: 0, pause: false }, + viewMode: 'view', + isLayerTOCOpen: false, + hideFilterActions: true, + mapCenter: { lon: 11, lat: 47, zoom: 0 }, + disableInteractive: true, + disableTooltipControl: true, + hideToolbarOverlay: true, + }; + + const embeddableRoot: React.RefObject = React.createRef(); + + return ( + +
+ + ); +}; + +EmbeddedMap.displayName = 'EmbeddedMap'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/low_poly_layer.json b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/low_poly_layer.json new file mode 100644 index 00000000000000..7a309cd01ebc77 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/low_poly_layer.json @@ -0,0 +1,2898 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + "34.21666", + "31.32333" + ], + [ + "35.98361", + "34.52750" + ], + [ + "34.65943", + "36.80527" + ], + [ + "32.77166", + "36.02888" + ], + [ + "29.67722", + "36.11833" + ], + [ + "27.25500", + "36.96500" + ], + [ + "27.51166", + "40.30555" + ], + [ + "33.33860", + "42.01985" + ], + [ + "38.35582", + "40.91027" + ], + [ + "41.77609", + "41.84193" + ], + [ + "41.59748", + "43.22151" + ], + [ + "45.16512", + "42.70333" + ], + [ + "47.91547", + "41.22499" + ], + [ + "49.76062", + "42.71076" + ], + [ + "49.44831", + "45.53038" + ], + [ + "47.30249", + "50.03194" + ], + [ + "52.34180", + "51.78075" + ], + [ + "55.69249", + "50.53249" + ], + [ + "58.33777", + "51.15610" + ], + [ + "57.97027", + "54.38819" + ], + [ + "59.64166", + "55.55867" + ], + [ + "57.22169", + "56.85096" + ], + [ + "59.44912", + "58.48804" + ], + [ + "59.57756", + "63.93287" + ], + [ + "66.10887", + "67.48123" + ], + [ + "64.52222", + "68.90305" + ], + [ + "67.05498", + "68.85637" + ], + [ + "69.32735", + "72.94540" + ], + [ + "73.52553", + "71.81582" + ], + [ + "80.82610", + "72.08693" + ], + [ + "80.51860", + "73.57346" + ], + [ + "89.25278", + "75.50305" + ], + [ + "97.18359", + "75.92804" + ], + [ + "104.07138", + "77.73221" + ], + [ + "111.10387", + "76.75526" + ], + [ + "113.47054", + "73.50096" + ], + [ + "118.63443", + "73.57166" + ], + [ + "131.53580", + "70.87776" + ], + [ + "137.45190", + "71.34109" + ], + [ + "141.02414", + "72.58582" + ], + [ + "149.18524", + "72.22249" + ], + [ + "152.53830", + "70.83777" + ], + [ + "159.72968", + "69.83472" + ], + [ + "170.61194", + "68.75633" + ], + [ + "170.47189", + "70.13416" + ], + [ + "180.00000", + "68.98010" + ], + [ + "180.00000", + "65.06891" + ], + [ + "179.55373", + "62.61971" + ], + [ + "173.54178", + "61.74430" + ], + [ + "170.64194", + "60.41750" + ], + [ + "163.36023", + "59.82388" + ], + [ + "161.93858", + "58.06763" + ], + [ + "163.34996", + "56.19596" + ], + [ + "156.74524", + "51.07791" + ], + [ + "155.54413", + "55.30360" + ], + [ + "155.94206", + "56.65353" + ], + [ + "161.91248", + "60.41972" + ], + [ + "159.24747", + "61.92222" + ], + [ + "152.35718", + "59.02332" + ], + [ + "143.21109", + "59.37666" + ], + [ + "137.72580", + "56.17500" + ], + [ + "137.29327", + "54.07500" + ], + [ + "141.41483", + "53.29361" + ], + [ + "140.17609", + "48.45013" + ], + [ + "135.42233", + "43.75611" + ], + [ + "133.15485", + "42.68263" + ], + [ + "131.81052", + "43.32555" + ], + [ + "129.70204", + "40.83069" + ], + [ + "127.51763", + "39.73957" + ], + [ + "129.42944", + "37.05986" + ], + [ + "129.23749", + "35.18990" + ], + [ + "126.37556", + "34.79138" + ], + [ + "126.38860", + "37.88721" + ], + [ + "124.32395", + "39.91589" + ], + [ + "121.64804", + "38.99638" + ], + [ + "121.17747", + "40.92194" + ], + [ + "118.11053", + "38.14639" + ], + [ + "120.82054", + "36.64527" + ], + [ + "120.24873", + "34.31145" + ], + [ + "121.84693", + "30.85305" + ], + [ + "120.93526", + "27.98222" + ], + [ + "119.58074", + "25.67996" + ], + [ + "116.48172", + "22.93902" + ], + [ + "112.28194", + "21.70139" + ], + [ + "107.36693", + "21.26527" + ], + [ + "105.63857", + "18.89065" + ], + [ + "108.82916", + "15.42194" + ], + [ + "109.46186", + "12.86097" + ], + [ + "109.02168", + "11.36225" + ], + [ + "104.79893", + "8.79222" + ], + [ + "104.98177", + "10.10444" + ], + [ + "100.97635", + "13.46281" + ], + [ + "99.15082", + "10.36472" + ], + [ + "100.57809", + "7.22014" + ], + [ + "103.18192", + "5.28278" + ], + [ + "103.37455", + "1.53347" + ], + [ + "101.28574", + "2.84354" + ], + [ + "100.35553", + "5.96389" + ], + [ + "98.27415", + "8.27444" + ], + [ + "98.74720", + "11.67486" + ], + [ + "97.72457", + "15.84666" + ], + [ + "95.42859", + "15.72972" + ], + [ + "93.72436", + "19.93243" + ], + [ + "91.70444", + "22.48055" + ], + [ + "86.96332", + "21.38194" + ], + [ + "86.42123", + "19.98493" + ], + [ + "80.27943", + "15.69917" + ], + [ + "79.85811", + "10.28583" + ], + [ + "76.99860", + "8.36527" + ], + [ + "74.85526", + "12.75500" + ], + [ + "73.44748", + "16.05861" + ], + [ + "72.56485", + "21.37506" + ], + [ + "70.82513", + "20.69597" + ], + [ + "66.50005", + "25.40381" + ], + [ + "61.76083", + "25.03208" + ], + [ + "57.31909", + "25.77146" + ], + [ + "56.80888", + "27.12361" + ], + [ + "54.78846", + "26.49041" + ], + [ + "51.43027", + "27.93777" + ], + [ + "50.63916", + "29.47042" + ], + [ + "47.95943", + "30.03305" + ], + [ + "48.83887", + "27.61972" + ], + [ + "51.28236", + "24.30000" + ], + [ + "53.58777", + "24.04417" + ], + [ + "55.85944", + "25.72042" + ], + [ + "57.17131", + "23.93444" + ], + [ + "59.82861", + "22.29166" + ], + [ + "57.80569", + "18.97097" + ], + [ + "55.03194", + "17.01472" + ], + [ + "52.18916", + "15.60528" + ], + [ + "45.04232", + "12.75239" + ], + [ + "43.47888", + "12.67500" + ], + [ + "42.78933", + "16.46083" + ], + [ + "40.75694", + "19.76417" + ], + [ + "39.17486", + "21.10402" + ], + [ + "39.06277", + "22.58333" + ], + [ + "35.16055", + "28.05666" + ], + [ + "34.21666", + "31.32333" + ] + ] + ], + [ + [ + [ + "-169.69496", + "66.06806" + ], + [ + "-173.67308", + "64.34679" + ], + [ + "-179.32083", + "65.53012" + ], + [ + "-180.00000", + "65.06891" + ], + [ + "-180.00000", + "68.98010" + ], + [ + "-169.69496", + "66.06806" + ] + ] + ], + [ + [ + [ + "139.93851", + "40.42860" + ], + [ + "142.06970", + "39.54666" + ], + [ + "140.95358", + "38.14805" + ], + [ + "140.33218", + "35.12985" + ], + [ + "137.02879", + "34.56784" + ], + [ + "136.71246", + "36.75139" + ], + [ + "139.42622", + "38.15458" + ], + [ + "139.93851", + "40.42860" + ] + ] + ], + [ + [ + [ + "119.89259", + "15.80112" + ], + [ + "120.58527", + "18.51139" + ], + [ + "122.51833", + "17.04389" + ], + [ + "121.38026", + "15.30250" + ], + [ + "119.89259", + "15.80112" + ] + ] + ], + [ + [ + [ + "122.32916", + "7.30833" + ], + [ + "126.18610", + "9.24277" + ], + [ + "125.37762", + "6.72361" + ], + [ + "123.45888", + "7.81055" + ], + [ + "122.32916", + "7.30833" + ] + ] + ], + [ + [ + [ + "111.89638", + "-3.57389" + ], + [ + "110.23193", + "-2.97111" + ], + [ + "108.84549", + "0.81056" + ], + [ + "109.64857", + "2.07341" + ], + [ + "113.01054", + "3.16055" + ], + [ + "115.37886", + "4.91167" + ], + [ + "116.75417", + "7.01805" + ], + [ + "119.27582", + "5.34500" + ], + [ + "117.27540", + "3.22000" + ], + [ + "117.87192", + "1.87667" + ], + [ + "117.44479", + "-0.52397" + ], + [ + "115.96624", + "-3.60875" + ], + [ + "113.03471", + "-2.98972" + ], + [ + "111.89638", + "-3.57389" + ] + ] + ], + [ + [ + [ + "102.97601", + "0.64348" + ], + [ + "103.36081", + "-0.70222" + ], + [ + "106.05525", + "-3.03139" + ], + [ + "105.72887", + "-5.89826" + ], + [ + "102.32610", + "-4.00611" + ], + [ + "100.90555", + "-2.31944" + ], + [ + "98.70383", + "1.55979" + ], + [ + "95.53108", + "4.68278" + ], + [ + "97.51483", + "5.24944" + ], + [ + "100.41219", + "2.29306" + ], + [ + "102.97601", + "0.64348" + ] + ] + ], + [ + [ + [ + "120.82723", + "1.23406" + ], + [ + "120.01999", + "-0.07528" + ], + [ + "122.47623", + "-3.16090" + ], + [ + "120.32888", + "-5.51208" + ], + [ + "119.35491", + "-5.40007" + ], + [ + "118.88860", + "-2.89319" + ], + [ + "119.77805", + "0.22972" + ], + [ + "120.82723", + "1.23406" + ] + ] + ], + [ + [ + [ + "136.04913", + "-2.69806" + ], + [ + "137.87579", + "-1.47306" + ], + [ + "144.51373", + "-3.82222" + ], + [ + "145.76639", + "-5.48528" + ], + [ + "147.46661", + "-5.97086" + ], + [ + "146.08969", + "-8.09111" + ], + [ + "144.21738", + "-7.79465" + ], + [ + "143.36510", + "-9.01222" + ], + [ + "141.11996", + "-9.23097" + ], + [ + "139.09454", + "-7.56181" + ], + [ + "138.06525", + "-5.40896" + ], + [ + "135.20468", + "-4.45972" + ], + [ + "132.72275", + "-2.81722" + ], + [ + "131.25555", + "-0.82278" + ], + [ + "134.02950", + "-0.96694" + ], + [ + "134.99495", + "-3.33653" + ], + [ + "136.04913", + "-2.69806" + ] + ] + ], + [ + [ + [ + "110.05640", + "-7.89751" + ], + [ + "106.56721", + "-7.41694" + ], + [ + "106.07582", + "-5.88194" + ], + [ + "110.39360", + "-6.97903" + ], + [ + "110.05640", + "-7.89751" + ] + ] + ] + ] + }, + "properties": { + "CONTINENT": "Asia" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + "-25.28167", + "71.39166" + ], + [ + "-23.56056", + "70.10609" + ], + [ + "-26.36333", + "68.66748" + ], + [ + "-31.99916", + "68.09526" + ], + [ + "-34.71999", + "66.33832" + ], + [ + "-41.15541", + "64.96235" + ], + [ + "-43.08722", + "60.10027" + ], + [ + "-47.68986", + "61.00680" + ], + [ + "-50.31562", + "62.49430" + ], + [ + "-53.23333", + "65.68283" + ], + [ + "-53.62778", + "67.81470" + ], + [ + "-50.58930", + "69.92373" + ], + [ + "-54.68694", + "72.36721" + ], + [ + "-58.15958", + "75.50860" + ], + [ + "-68.50056", + "76.08693" + ], + [ + "-72.55222", + "78.52110" + ], + [ + "-60.80666", + "81.87997" + ], + [ + "-30.38833", + "83.60220" + ], + [ + "-16.00500", + "80.72859" + ], + [ + "-22.03695", + "77.68568" + ], + [ + "-19.33681", + "75.40207" + ], + [ + "-24.46305", + "73.53581" + ], + [ + "-25.28167", + "71.39166" + ] + ] + ], + [ + [ + [ + "-87.64890", + "76.33804" + ], + [ + "-86.47916", + "79.76167" + ], + [ + "-90.43666", + "81.88750" + ], + [ + "-70.26001", + "83.11388" + ], + [ + "-61.07639", + "82.32083" + ], + [ + "-78.78194", + "76.57221" + ], + [ + "-87.64890", + "76.33804" + ] + ] + ], + [ + [ + [ + "-123.83389", + "73.70027" + ], + [ + "-115.31903", + "73.47707" + ], + [ + "-123.29306", + "71.14610" + ], + [ + "-123.83389", + "73.70027" + ] + ] + ], + [ + [ + [ + "-65.32806", + "62.66610" + ], + [ + "-68.61583", + "62.26389" + ], + [ + "-77.33667", + "65.17609" + ], + [ + "-72.25835", + "67.24803" + ], + [ + "-77.30506", + "69.83395" + ], + [ + "-85.87465", + "70.07943" + ], + [ + "-89.90348", + "71.35304" + ], + [ + "-89.03958", + "73.25499" + ], + [ + "-81.57251", + "73.71971" + ], + [ + "-67.21986", + "69.94081" + ], + [ + "-67.23819", + "68.35790" + ], + [ + "-61.26458", + "66.62609" + ], + [ + "-65.56204", + "64.73154" + ], + [ + "-65.32806", + "62.66610" + ] + ] + ], + [ + [ + [ + "-105.02444", + "72.21999" + ], + [ + "-100.99973", + "70.17276" + ], + [ + "-101.85139", + "68.98442" + ], + [ + "-113.04173", + "68.49374" + ], + [ + "-116.53221", + "69.40887" + ], + [ + "-119.13445", + "71.77457" + ], + [ + "-114.66666", + "73.37247" + ], + [ + "-105.02444", + "72.21999" + ] + ] + ], + [ + [ + [ + "-77.36667", + "8.67500" + ], + [ + "-77.88972", + "7.22889" + ], + [ + "-79.69778", + "8.86666" + ], + [ + "-81.73862", + "8.16250" + ], + [ + "-85.65668", + "9.90500" + ], + [ + "-85.66959", + "11.05500" + ], + [ + "-87.93779", + "13.15639" + ], + [ + "-91.38474", + "13.97889" + ], + [ + "-93.93861", + "16.09389" + ], + [ + "-96.47612", + "15.64361" + ], + [ + "-103.45001", + "18.31361" + ], + [ + "-105.67834", + "20.38305" + ], + [ + "-105.18945", + "21.43750" + ], + [ + "-106.91570", + "23.86514" + ], + [ + "-109.43750", + "25.82027" + ], + [ + "-109.44431", + "26.71555" + ], + [ + "-112.16195", + "28.97139" + ], + [ + "-113.09167", + "31.22972" + ], + [ + "-115.69667", + "29.77423" + ], + [ + "-117.40944", + "33.24416" + ], + [ + "-120.60583", + "34.55860" + ], + [ + "-124.33118", + "40.27246" + ], + [ + "-124.52444", + "42.86610" + ], + [ + "-123.87161", + "45.52898" + ], + [ + "-124.71431", + "48.39708" + ], + [ + "-124.03510", + "49.91801" + ], + [ + "-127.17315", + "50.92221" + ], + [ + "-130.88640", + "55.70791" + ], + [ + "-133.81302", + "57.97293" + ], + [ + "-136.65891", + "58.21652" + ], + [ + "-140.40335", + "59.69804" + ], + [ + "-146.75543", + "60.95249" + ], + [ + "-154.23567", + "58.13069" + ], + [ + "-157.55139", + "58.38777" + ], + [ + "-165.42244", + "60.55215" + ], + [ + "-164.40112", + "63.21499" + ], + [ + "-168.13196", + "65.66296" + ], + [ + "-161.66779", + "67.02054" + ], + [ + "-166.82362", + "68.34873" + ], + [ + "-156.59673", + "71.35144" + ], + [ + "-151.22986", + "70.37296" + ], + [ + "-143.21555", + "70.11026" + ], + [ + "-137.25500", + "68.94832" + ], + [ + "-127.18096", + "70.27638" + ], + [ + "-114.06652", + "68.46970" + ], + [ + "-112.39584", + "67.67915" + ], + [ + "-98.11124", + "67.83887" + ], + [ + "-90.43639", + "68.87442" + ], + [ + "-85.55499", + "69.85970" + ], + [ + "-81.33570", + "69.18498" + ], + [ + "-81.50222", + "67.00096" + ], + [ + "-85.89726", + "66.16802" + ], + [ + "-87.98736", + "64.18845" + ], + [ + "-92.71001", + "62.46583" + ], + [ + "-94.78972", + "59.09222" + ], + [ + "-92.41875", + "57.33270" + ], + [ + "-88.81500", + "56.82444" + ], + [ + "-85.00195", + "55.29666" + ], + [ + "-82.30777", + "55.14888" + ], + [ + "-82.27390", + "52.95638" + ], + [ + "-78.57945", + "52.11138" + ], + [ + "-79.76181", + "54.65166" + ], + [ + "-76.67979", + "56.03645" + ], + [ + "-78.57299", + "58.62888" + ], + [ + "-77.50835", + "62.56166" + ], + [ + "-73.68346", + "62.47999" + ], + [ + "-70.14848", + "61.08458" + ], + [ + "-67.56610", + "58.22360" + ], + [ + "-64.74538", + "60.23075" + ], + [ + "-61.09055", + "55.84415" + ], + [ + "-57.34969", + "54.57496" + ], + [ + "-56.95160", + "51.42458" + ], + [ + "-60.00500", + "50.24888" + ], + [ + "-66.44903", + "50.26777" + ], + [ + "-64.21167", + "48.88499" + ], + [ + "-64.90430", + "46.84597" + ], + [ + "-63.66708", + "45.81666" + ], + [ + "-70.19187", + "43.57555" + ], + [ + "-70.72610", + "41.72777" + ], + [ + "-74.13390", + "40.70082" + ], + [ + "-75.96083", + "37.15221" + ], + [ + "-76.34326", + "34.88194" + ], + [ + "-78.82750", + "33.73027" + ], + [ + "-81.48843", + "31.11347" + ], + [ + "-80.03534", + "26.79569" + ], + [ + "-81.73659", + "25.95944" + ], + [ + "-84.01098", + "30.09764" + ], + [ + "-88.98083", + "30.41833" + ], + [ + "-94.75417", + "29.36791" + ], + [ + "-97.56041", + "26.84208" + ], + [ + "-97.74223", + "22.01250" + ], + [ + "-95.80112", + "18.74500" + ], + [ + "-94.46918", + "18.14625" + ], + [ + "-90.73167", + "19.36153" + ], + [ + "-90.27972", + "21.06305" + ], + [ + "-86.82973", + "21.42923" + ], + [ + "-88.28250", + "17.62389" + ], + [ + "-88.13696", + "15.68285" + ], + [ + "-84.26015", + "15.82597" + ], + [ + "-83.18695", + "14.32389" + ], + [ + "-83.84751", + "11.17458" + ], + [ + "-82.24278", + "9.00236" + ], + [ + "-79.53445", + "9.62014" + ], + [ + "-77.36667", + "8.67500" + ] + ] + ], + [ + [ + [ + "-55.19333", + "46.98499" + ], + [ + "-59.40361", + "47.89423" + ], + [ + "-56.68250", + "51.33943" + ], + [ + "-55.56114", + "49.36818" + ], + [ + "-52.83465", + "48.09965" + ], + [ + "-55.19333", + "46.98499" + ] + ] + ], + [ + [ + [ + "-73.03644", + "18.45622" + ], + [ + "-72.79834", + "19.94278" + ], + [ + "-69.94932", + "19.67680" + ], + [ + "-68.89528", + "18.39639" + ], + [ + "-73.03644", + "18.45622" + ] + ] + ] + ] + }, + "properties": { + "CONTINENT": "North America" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + "64.52222", + "68.90305" + ], + [ + "66.10887", + "67.48123" + ], + [ + "59.57756", + "63.93287" + ], + [ + "59.44912", + "58.48804" + ], + [ + "57.22169", + "56.85096" + ], + [ + "59.64166", + "55.55867" + ], + [ + "57.97027", + "54.38819" + ], + [ + "58.33777", + "51.15610" + ], + [ + "55.69249", + "50.53249" + ], + [ + "52.34180", + "51.78075" + ], + [ + "47.30249", + "50.03194" + ], + [ + "49.44831", + "45.53038" + ], + [ + "49.76062", + "42.71076" + ], + [ + "47.91547", + "41.22499" + ], + [ + "45.16512", + "42.70333" + ], + [ + "41.59748", + "43.22151" + ], + [ + "39.94553", + "43.39693" + ], + [ + "34.70249", + "46.17582" + ], + [ + "30.83277", + "46.54832" + ], + [ + "28.78083", + "44.66096" + ], + [ + "28.01305", + "41.98222" + ], + [ + "26.36041", + "40.95388" + ], + [ + "22.59500", + "40.01221" + ], + [ + "23.96055", + "38.28166" + ], + [ + "22.15246", + "37.01854" + ], + [ + "19.30721", + "40.64531" + ], + [ + "19.59771", + "41.80611" + ], + [ + "15.15167", + "44.19639" + ], + [ + "13.02958", + "41.26014" + ], + [ + "8.74722", + "44.42805" + ], + [ + "6.16528", + "43.05055" + ], + [ + "4.05625", + "43.56277" + ], + [ + "3.20167", + "41.89278" + ], + [ + "0.99306", + "41.04805" + ], + [ + "0.20722", + "38.73221" + ], + [ + "-2.12292", + "36.73347" + ], + [ + "-5.61361", + "36.00610" + ], + [ + "-6.95992", + "37.22184" + ], + [ + "-8.98924", + "37.02631" + ], + [ + "-9.49083", + "38.79388" + ], + [ + "-8.66014", + "40.69111" + ], + [ + "-9.16972", + "43.18583" + ], + [ + "-1.44389", + "43.64055" + ], + [ + "-1.11463", + "46.31658" + ], + [ + "-2.68528", + "48.50166" + ], + [ + "1.43875", + "50.10083" + ], + [ + "5.59917", + "53.30028" + ], + [ + "13.80854", + "53.85479" + ], + [ + "21.24506", + "54.95506" + ], + [ + "21.05223", + "56.81749" + ], + [ + "23.43159", + "59.95382" + ], + [ + "21.42416", + "60.57930" + ], + [ + "21.58500", + "64.43971" + ], + [ + "17.09861", + "61.60278" + ], + [ + "19.07264", + "59.73819" + ], + [ + "16.37982", + "56.66333" + ], + [ + "12.46007", + "56.29666" + ], + [ + "10.51569", + "59.30624" + ], + [ + "8.12750", + "58.09888" + ], + [ + "5.50847", + "58.66764" + ], + [ + "4.94944", + "61.41041" + ], + [ + "9.54528", + "63.76611" + ], + [ + "15.28833", + "68.03055" + ], + [ + "21.30000", + "70.24693" + ], + [ + "28.20778", + "71.07999" + ], + [ + "32.80605", + "69.30277" + ], + [ + "43.75180", + "67.31152" + ], + [ + "53.60437", + "68.90818" + ], + [ + "64.52222", + "68.90305" + ] + ] + ], + [ + [ + [ + "-13.49944", + "65.06915" + ], + [ + "-18.77500", + "63.39139" + ], + [ + "-22.04556", + "64.04666" + ], + [ + "-22.42167", + "66.43332" + ], + [ + "-16.41736", + "66.27603" + ], + [ + "-13.49944", + "65.06915" + ] + ] + ], + [ + [ + [ + "-4.19667", + "57.48583" + ], + [ + "-0.07931", + "54.11340" + ], + [ + "0.25389", + "50.73861" + ], + [ + "-3.43722", + "50.60500" + ], + [ + "-4.19639", + "53.20611" + ], + [ + "-2.89979", + "53.72499" + ], + [ + "-6.22778", + "56.69722" + ], + [ + "-4.19667", + "57.48583" + ] + ] + ], + [ + [ + [ + "12.44167", + "37.80611" + ], + [ + "15.64794", + "38.26458" + ], + [ + "15.08139", + "36.64916" + ], + [ + "12.44167", + "37.80611" + ] + ] + ] + ] + }, + "properties": { + "CONTINENT": "Europe" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + "34.21666", + "31.32333" + ], + [ + "34.90380", + "29.48671" + ], + [ + "33.93833", + "26.65528" + ], + [ + "36.88625", + "22.05319" + ], + [ + "37.43569", + "18.85389" + ], + [ + "38.58902", + "18.06680" + ], + [ + "39.71805", + "15.08805" + ], + [ + "41.17222", + "14.63069" + ], + [ + "43.32750", + "12.47673" + ], + [ + "44.27833", + "10.44778" + ], + [ + "50.09319", + "11.51458" + ], + [ + "51.14555", + "10.63361" + ], + [ + "48.00055", + "4.52306" + ], + [ + "46.02555", + "2.43722" + ], + [ + "43.48861", + "0.65000" + ], + [ + "40.12548", + "-3.26569" + ], + [ + "38.77611", + "-6.03972" + ], + [ + "40.38777", + "-11.31778" + ], + [ + "40.57833", + "-15.49889" + ], + [ + "34.89069", + "-19.86042" + ], + [ + "35.45611", + "-24.16945" + ], + [ + "32.81111", + "-25.61209" + ], + [ + "32.39444", + "-28.53139" + ], + [ + "27.90000", + "-33.04056" + ], + [ + "24.82472", + "-34.20167" + ], + [ + "22.53916", + "-34.01118" + ], + [ + "20.00000", + "-34.82200" + ], + [ + "17.84750", + "-32.83083" + ], + [ + "18.21791", + "-31.73458" + ], + [ + "15.09500", + "-26.73528" + ], + [ + "14.51139", + "-22.55278" + ], + [ + "11.76764", + "-17.98820" + ], + [ + "11.73125", + "-15.85070" + ], + [ + "13.84944", + "-10.95611" + ], + [ + "13.39180", + "-8.39375" + ], + [ + "11.77417", + "-4.54264" + ], + [ + "9.70250", + "-2.44792" + ], + [ + "9.29833", + "-0.37167" + ], + [ + "9.96514", + "3.08521" + ], + [ + "8.89861", + "4.58833" + ], + [ + "5.93583", + "4.33833" + ], + [ + "4.41021", + "6.35993" + ], + [ + "1.46889", + "6.18639" + ], + [ + "-2.05889", + "4.73083" + ], + [ + "-4.46806", + "5.29556" + ], + [ + "-7.43639", + "4.34917" + ], + [ + "-9.23889", + "5.12278" + ], + [ + "-12.50417", + "7.38861" + ], + [ + "-13.49313", + "9.56008" + ], + [ + "-15.00542", + "10.77194" + ], + [ + "-17.17556", + "14.65444" + ], + [ + "-16.03945", + "17.73458" + ], + [ + "-16.91625", + "21.94542" + ], + [ + "-12.96271", + "27.92048" + ], + [ + "-11.51195", + "28.30375" + ], + [ + "-9.64097", + "30.16500" + ], + [ + "-8.53833", + "33.25055" + ], + [ + "-6.84306", + "34.01861" + ], + [ + "-5.91874", + "35.79065" + ], + [ + "-1.97972", + "35.07333" + ], + [ + "1.18250", + "36.51221" + ], + [ + "9.85868", + "37.32833" + ], + [ + "11.12667", + "35.24194" + ], + [ + "11.17430", + "33.21006" + ], + [ + "15.16583", + "32.39861" + ], + [ + "15.75430", + "31.38972" + ], + [ + "18.95750", + "30.27639" + ], + [ + "20.56763", + "32.56091" + ], + [ + "29.03500", + "30.82417" + ], + [ + "30.35545", + "31.50284" + ], + [ + "34.21666", + "31.32333" + ] + ] + ], + [ + [ + [ + "48.03140", + "-14.06341" + ], + [ + "49.94333", + "-13.03945" + ], + [ + "50.48277", + "-15.40583" + ], + [ + "49.36833", + "-18.35139" + ], + [ + "47.13305", + "-24.92806" + ], + [ + "44.01708", + "-24.98083" + ], + [ + "43.23888", + "-22.28250" + ], + [ + "44.48277", + "-19.96584" + ], + [ + "43.93139", + "-17.50056" + ], + [ + "44.87360", + "-16.21028" + ], + [ + "48.03140", + "-14.06341" + ] + ] + ] + ] + }, + "properties": { + "CONTINENT": "Africa" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + "-77.88972", + "7.22889" + ], + [ + "-77.36667", + "8.67500" + ], + [ + "-75.63432", + "9.44819" + ], + [ + "-74.86081", + "11.12549" + ], + [ + "-68.84368", + "11.44708" + ], + [ + "-68.11424", + "10.48493" + ], + [ + "-61.87959", + "10.72833" + ], + [ + "-61.61987", + "9.90528" + ], + [ + "-57.51919", + "6.27077" + ], + [ + "-52.97320", + "5.47305" + ], + [ + "-51.25931", + "4.15250" + ], + [ + "-49.90320", + "1.17444" + ], + [ + "-51.92751", + "-1.33486" + ], + [ + "-48.42722", + "-1.66028" + ], + [ + "-47.28556", + "-0.59917" + ], + [ + "-42.23584", + "-2.83778" + ], + [ + "-39.99875", + "-2.84653" + ], + [ + "-37.17445", + "-4.91861" + ], + [ + "-35.47973", + "-5.16611" + ], + [ + "-34.83129", + "-6.98180" + ], + [ + "-35.32751", + "-9.22889" + ], + [ + "-39.05709", + "-13.38028" + ], + [ + "-38.87195", + "-15.87417" + ], + [ + "-39.70403", + "-19.42361" + ], + [ + "-42.03445", + "-22.91917" + ], + [ + "-44.67521", + "-23.05570" + ], + [ + "-48.02612", + "-25.01500" + ], + [ + "-48.84251", + "-28.61778" + ], + [ + "-52.21764", + "-31.74500" + ], + [ + "-54.14077", + "-34.66466" + ], + [ + "-56.15834", + "-34.92722" + ], + [ + "-56.67834", + "-36.92361" + ], + [ + "-58.30112", + "-38.48500" + ], + [ + "-62.06875", + "-39.50848" + ], + [ + "-62.39001", + "-40.90195" + ], + [ + "-65.13014", + "-40.84417" + ], + [ + "-65.24945", + "-44.31306" + ], + [ + "-67.58435", + "-46.00030" + ], + [ + "-65.78979", + "-47.96584" + ], + [ + "-68.94112", + "-50.38806" + ], + [ + "-68.99014", + "-51.62445" + ], + [ + "-72.11501", + "-53.68764" + ], + [ + "-74.28924", + "-50.48049" + ], + [ + "-74.74139", + "-47.71146" + ], + [ + "-72.61389", + "-44.47278" + ], + [ + "-73.99432", + "-40.96695" + ], + [ + "-73.22404", + "-39.41688" + ], + [ + "-73.67709", + "-37.34729" + ], + [ + "-71.44667", + "-32.66500" + ], + [ + "-71.69585", + "-30.50667" + ], + [ + "-70.91389", + "-27.62445" + ], + [ + "-70.05334", + "-21.42565" + ], + [ + "-70.31202", + "-18.43750" + ], + [ + "-71.49424", + "-17.30223" + ], + [ + "-75.05139", + "-15.46597" + ], + [ + "-76.39480", + "-13.88417" + ], + [ + "-78.99459", + "-8.21965" + ], + [ + "-81.17473", + "-6.08667" + ], + [ + "-81.27640", + "-4.28083" + ], + [ + "-79.95632", + "-3.20778" + ], + [ + "-80.91279", + "-1.03653" + ], + [ + "-80.10084", + "0.77028" + ], + [ + "-78.88929", + "1.23837" + ], + [ + "-77.43445", + "4.03139" + ], + [ + "-77.88972", + "7.22889" + ] + ] + ] + }, + "properties": { + "CONTINENT": "South America" + } + }, + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + "177.91779", + "-38.94280" + ], + [ + "175.95523", + "-41.25528" + ], + [ + "173.75165", + "-39.27000" + ], + [ + "174.94025", + "-38.10111" + ], + [ + "177.91779", + "-38.94280" + ] + ] + ], + [ + [ + [ + "171.18524", + "-44.93833" + ], + [ + "169.45801", + "-46.62333" + ], + [ + "166.47690", + "-45.80972" + ], + [ + "168.37233", + "-44.04056" + ], + [ + "171.15166", + "-42.56042" + ], + [ + "172.63025", + "-40.51056" + ], + [ + "174.23636", + "-41.83722" + ], + [ + "171.18524", + "-44.93833" + ] + ] + ] + ] + }, + "properties": { + "CONTINENT": "Oceania" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + "151.54025", + "-24.04583" + ], + [ + "153.18192", + "-25.94944" + ], + [ + "153.62419", + "-28.66104" + ], + [ + "152.52969", + "-32.40361" + ], + [ + "151.45456", + "-33.31681" + ], + [ + "149.97163", + "-37.52222" + ], + [ + "146.87357", + "-38.65166" + ], + [ + "143.54295", + "-38.85923" + ], + [ + "140.52997", + "-38.00028" + ], + [ + "138.09225", + "-34.13493" + ], + [ + "135.49586", + "-34.61708" + ], + [ + "134.18414", + "-32.48666" + ], + [ + "131.14859", + "-31.47403" + ], + [ + "125.97227", + "-32.26674" + ], + [ + "123.73499", + "-33.77972" + ], + [ + "120.00499", + "-33.92889" + ], + [ + "117.93414", + "-35.12534" + ], + [ + "115.00895", + "-34.26243" + ], + [ + "115.73998", + "-31.86806" + ], + [ + "113.64346", + "-26.65431" + ], + [ + "113.38971", + "-24.42944" + ], + [ + "114.03027", + "-21.84167" + ], + [ + "116.70749", + "-20.64917" + ], + [ + "121.02748", + "-19.59222" + ], + [ + "122.95623", + "-16.58681" + ], + [ + "126.85790", + "-13.75097" + ], + [ + "129.08942", + "-14.89944" + ], + [ + "130.57927", + "-12.40465" + ], + [ + "132.67198", + "-11.50813" + ], + [ + "135.23135", + "-12.29445" + ], + [ + "135.45135", + "-14.93278" + ], + [ + "136.76581", + "-15.90445" + ], + [ + "140.83330", + "-17.45194" + ], + [ + "141.66553", + "-15.02653" + ], + [ + "141.59412", + "-12.53167" + ], + [ + "142.78830", + "-11.08056" + ], + [ + "143.78220", + "-14.41333" + ], + [ + "145.31580", + "-14.94555" + ], + [ + "146.27762", + "-18.88701" + ], + [ + "147.43192", + "-19.41236" + ], + [ + "150.81912", + "-22.73194" + ], + [ + "151.54025", + "-24.04583" + ] + ] + ] + }, + "properties": { + "CONTINENT": "Australia" + } + } + ] +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts new file mode 100644 index 00000000000000..1e8e5b6012a79d --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getLayerList } from './map_config'; +import { mockLayerList } from './__mocks__/mock'; +import { LocationPoint } from './embedded_map'; + +jest.mock('uuid', () => { + return { + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +describe('map_config', () => { + let upPoints: LocationPoint[]; + let downPoints: LocationPoint[]; + + beforeEach(() => { + upPoints = [ + { lat: '52.487239', lon: '13.399262' }, + { lat: '55.487239', lon: '13.399262' }, + { lat: '54.487239', lon: '14.399262' }, + ]; + downPoints = [ + { lat: '52.487239', lon: '13.399262' }, + { lat: '55.487239', lon: '13.399262' }, + { lat: '54.487239', lon: '14.399262' }, + ]; + }); + + describe('#getLayerList', () => { + test('it returns the low poly layer', () => { + const layerList = getLayerList(upPoints, downPoints); + expect(layerList).toStrictEqual(mockLayerList); + }); + }); +}); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts new file mode 100644 index 00000000000000..608df8b235f001 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/map_config.ts @@ -0,0 +1,167 @@ +/* + * 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 lowPolyLayerFeatures from './low_poly_layer.json'; +import { LocationPoint } from './embedded_map'; + +/** + * Returns `Source/Destination Point-to-point` Map LayerList configuration, with a source, + * destination, and line layer for each of the provided indexPatterns + * + */ +export const getLayerList = (upPoints: LocationPoint[], downPoints: LocationPoint[]) => { + return [getLowPolyLayer(), getDownPointsLayer(downPoints), getUpPointsLayer(upPoints)]; +}; + +export const getLowPolyLayer = () => { + return { + id: 'low_poly_layer', + label: 'World countries', + minZoom: 0, + maxZoom: 24, + alpha: 1, + sourceDescriptor: { + id: 'b7486535-171b-4d3b-bb2e-33c1a0a2854c', + type: 'GEOJSON_FILE', + __featureCollection: lowPolyLayerFeatures, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#cad3e4', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 0, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; + +export const getDownPointsLayer = (downPoints: LocationPoint[]) => { + const features = downPoints?.map(point => ({ + type: 'feature', + geometry: { + type: 'Point', + coordinates: [+point.lon, +point.lat], + }, + })); + return { + id: 'down_points', + label: 'Down Locations', + sourceDescriptor: { + type: 'GEOJSON_FILE', + __featureCollection: { + features, + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#BC261E', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; + +export const getUpPointsLayer = (upPoints: LocationPoint[]) => { + const features = upPoints?.map(point => ({ + type: 'feature', + geometry: { + type: 'Point', + coordinates: [+point.lon, +point.lat], + }, + })); + return { + id: 'up_points', + label: 'Up Locations', + sourceDescriptor: { + type: 'GEOJSON_FILE', + __featureCollection: { + features, + type: 'FeatureCollection', + }, + }, + visible: true, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { + color: '#98A2B2', + }, + }, + lineColor: { + type: 'STATIC', + options: { + color: '#fff', + }, + }, + lineWidth: { + type: 'STATIC', + options: { + size: 2, + }, + }, + iconSize: { + type: 'STATIC', + options: { + size: 6, + }, + }, + }, + }, + type: 'VECTOR', + }; +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/translations.ts new file mode 100644 index 00000000000000..a5f68228efb1a9 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const MAP_TITLE = i18n.translate( + 'xpack.uptime.components.embeddables.embeddedMap.embeddablePanelTitle', + { + defaultMessage: 'Monitor Observer Location Map', + } +); diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/types.ts b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/types.ts new file mode 100644 index 00000000000000..5cac204ffb0713 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Query } from 'src/plugins/data/common'; +import { TimeRange } from 'src/plugins/data/public'; +import { + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; + +import { esFilters } from '../../../../../../../../../src/plugins/data/public'; + +export interface MapEmbeddableInput extends EmbeddableInput { + filters: esFilters.Filter[]; + query: Query; + refreshConfig: { + isPaused: boolean; + interval: number; + }; + timeRange?: TimeRange; +} + +export interface CustomProps { + setLayerList: Function; +} + +export type MapEmbeddable = IEmbeddable & CustomProps; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/index.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/index.tsx new file mode 100644 index 00000000000000..1f4b88b971c4ce --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/index.tsx @@ -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 * from './location_map'; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.tsx new file mode 100644 index 00000000000000..b271632cb631fd --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/location_map.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 styled from 'styled-components'; +import { EmbeddedMap, LocationPoint } from './embeddables/embedded_map'; + +const MapPanel = styled.div` + height: 400px; + width: 520px; +`; + +interface LocationMapProps { + monitorLocations: any; +} + +export const LocationMap = ({ monitorLocations }: LocationMapProps) => { + const upPoints: LocationPoint[] = []; + const downPoints: LocationPoint[] = []; + + if (monitorLocations?.locations) { + monitorLocations.locations.forEach((item: any) => { + if (item.summary.down === 0) { + upPoints.push(item.geo.location); + } else { + downPoints.push(item.geo.location); + } + }); + } + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap index e52977749142d9..376f1aa54f532b 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_list/__tests__/__snapshots__/monitor_page_link.test.tsx.snap @@ -4,7 +4,6 @@ exports[`MonitorPageLink component renders a help link when link parameters pres @@ -14,7 +13,6 @@ exports[`MonitorPageLink component renders the link properly 1`] = ` diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx index 7ef6c3ed1e4bfb..f36f0dff6745f8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/monitor_status_bar.tsx @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink, EuiPanel } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; @@ -16,6 +15,7 @@ import { monitorStatusBarQuery } from '../../../queries'; import { EmptyStatusBar } from '../empty_status_bar'; import { convertMicrosecondsToMilliseconds } from '../../../lib/helper'; import { MonitorSSLCertificate } from './monitor_ssl_certificate'; +import * as labels from './translations'; interface MonitorStatusBarQueryResult { monitorStatus?: Ping[]; @@ -28,58 +28,33 @@ interface MonitorStatusBarProps { type Props = MonitorStatusBarProps & UptimeGraphQLQueryProps; export const MonitorStatusBarComponent = ({ data, monitorId }: Props) => { - if (data && data.monitorStatus && data.monitorStatus.length) { + if (data?.monitorStatus?.length) { const { monitor, timestamp, tls } = data.monitorStatus[0]; const duration: number | undefined = get(monitor, 'duration.us', undefined); const status = get<'up' | 'down'>(monitor, 'status', 'down'); const full = get(data.monitorStatus[0], 'url.full'); return ( - + <> - {status === 'up' - ? i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { - defaultMessage: 'Up', - }) - : i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', { - defaultMessage: 'Down', - })} + {status === 'up' ? labels.upLabel : labels.downLabel} - + {full} {!!duration && ( - + { /> )} - + {moment(new Date(timestamp).valueOf()).fromNow()} - + ); } - return ( - - ); + return ; }; export const MonitorStatusBar = withUptimeGraphQL< diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/translations.ts new file mode 100644 index 00000000000000..1c2844f4f6ccf2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_bar/translations.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 { i18n } from '@kbn/i18n'; + +export const healthStatusMessageAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', + { + defaultMessage: 'Monitor status', + } +); + +export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { + defaultMessage: 'Up', +}); + +export const downLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', + { + defaultMessage: 'Down', + } +); + +export const monitorUrlLinkAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', + { + defaultMessage: 'Monitor URL link', + } +); + +export const durationTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', + { + defaultMessage: 'Monitor duration in milliseconds', + } +); + +export const timestampFromNowTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', + { + defaultMessage: 'Time since last check', + } +); + +export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { + defaultMessage: 'Loading…', +}); 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 new file mode 100644 index 00000000000000..234586e0b51f13 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { connect } from 'react-redux'; +import { AppState } from '../../../state'; +import { getMonitorLocations } from '../../../state/selectors'; +import { fetchMonitorLocations } from '../../../state/actions/monitor'; +import { MonitorStatusDetailsComponent } from './monitor_status_details'; + +const mapStateToProps = (state: AppState, { monitorId }: any) => ({ + monitorLocations: getMonitorLocations(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'; 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 new file mode 100644 index 00000000000000..cf337eaec4bbc1 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/monitor_status_details.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { LocationMap } from '../location_map'; +import { MonitorStatusBar } from '../monitor_status_bar'; + +interface MonitorStatusBarProps { + monitorId: string; + variables: any; + loadMonitorLocations: any; + monitorLocations: any; + dateStart: any; + dateEnd: any; +} + +export const MonitorStatusDetailsComponent = ({ + monitorId, + variables, + loadMonitorLocations, + monitorLocations, + dateStart, + dateEnd, +}: MonitorStatusBarProps) => { + useEffect(() => { + loadMonitorLocations(monitorId); + }, [monitorId, dateStart, dateEnd]); + + return ( + + + + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.ts b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.ts new file mode 100644 index 00000000000000..1c2844f4f6ccf2 --- /dev/null +++ b/x-pack/legacy/plugins/uptime/public/components/functional/monitor_status_details/translations.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 { i18n } from '@kbn/i18n'; + +export const healthStatusMessageAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessageAriaLabel', + { + defaultMessage: 'Monitor status', + } +); + +export const upLabel = i18n.translate('xpack.uptime.monitorStatusBar.healthStatusMessage.upLabel', { + defaultMessage: 'Up', +}); + +export const downLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.healthStatusMessage.downLabel', + { + defaultMessage: 'Down', + } +); + +export const monitorUrlLinkAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.monitorUrlLinkAriaLabel', + { + defaultMessage: 'Monitor URL link', + } +); + +export const durationTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.durationTextAriaLabel', + { + defaultMessage: 'Monitor duration in milliseconds', + } +); + +export const timestampFromNowTextAriaLabel = i18n.translate( + 'xpack.uptime.monitorStatusBar.timestampFromNowTextAriaLabel', + { + defaultMessage: 'Time since last check', + } +); + +export const loadingMessage = i18n.translate('xpack.uptime.monitorStatusBar.loadingMessage', { + defaultMessage: 'Loading…', +}); diff --git a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx index 86a0a9e4b0f0b9..8c5fced2f28649 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/monitor.tsx @@ -4,26 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore No typings for EuiSpacer - EuiSpacer, -} from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import { ApolloQueryResult, OperationVariables, QueryOptions } from 'apollo-client'; import gql from 'graphql-tag'; import React, { Fragment, useContext, useEffect, useState } from 'react'; import { getMonitorPageBreadcrumb } from '../breadcrumbs'; -import { - MonitorCharts, - MonitorPageTitle, - MonitorStatusBar, - PingList, -} from '../components/functional'; +import { MonitorCharts, MonitorPageTitle, PingList } from '../components/functional'; import { UMUpdateBreadcrumbs } from '../lib/lib'; import { UptimeSettingsContext } from '../contexts'; import { useUrlParams } from '../hooks'; import { stringifyUrlParams } from '../lib/helper/stringify_url_params'; import { useTrackPageview } from '../../../infra/public'; import { getTitle } from '../lib/helper/get_title'; +import { MonitorStatusDetails } from '../components/functional/monitor_status_details'; interface MonitorPageProps { logMonitorPageLoad: () => void; @@ -92,7 +85,12 @@ export const MonitorPage = ({ - + = (endpoint: string, params: T, options?: U) => Promise; +export interface QueryParams { + dateStart: string; + dateEnd: string; + filters?: string; + statusFilter?: string; +} diff --git a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts index d043cf71194720..0fb00b935342e6 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/monitor.ts @@ -6,7 +6,13 @@ import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; import { getApiPath } from '../../lib/helper'; -import { MonitorDetailsType, MonitorDetails } from '../../../common/runtime_types'; +import { + MonitorDetailsType, + MonitorDetails, + MonitorLocations, + MonitorLocationsType, +} from '../../../common/runtime_types'; +import { QueryParams } from '../actions/types'; interface ApiRequest { monitorId: string; @@ -27,3 +33,30 @@ export const fetchMonitorDetails = async ({ return data; }); }; + +type ApiParams = QueryParams & ApiRequest; + +export const fetchMonitorLocations = async ({ + monitorId, + basePath, + dateStart, + dateEnd, +}: ApiParams): Promise => { + const url = getApiPath(`/api/uptime/monitor/locations`, basePath); + + const params = { + dateStart, + dateEnd, + monitorId, + }; + const urlParams = new URLSearchParams(params).toString(); + const response = await fetch(`${url}?${urlParams}`); + + if (!response.ok) { + throw new Error(response.statusText); + } + return response.json().then(data => { + ThrowReporter.report(MonitorLocationsType.decode(data)); + return data; + }); +}; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts index 529b9041c90933..210004bb343bbe 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/monitor.ts @@ -10,8 +10,11 @@ import { FETCH_MONITOR_DETAILS, FETCH_MONITOR_DETAILS_SUCCESS, FETCH_MONITOR_DETAILS_FAIL, + FETCH_MONITOR_LOCATIONS, + FETCH_MONITOR_LOCATIONS_SUCCESS, + FETCH_MONITOR_LOCATIONS_FAIL, } from '../actions/monitor'; -import { fetchMonitorDetails } from '../api'; +import { fetchMonitorDetails, fetchMonitorLocations } from '../api'; import { getBasePath } from '../selectors'; function* monitorDetailsEffect(action: Action) { @@ -25,6 +28,18 @@ function* monitorDetailsEffect(action: Action) { } } +function* monitorLocationsEffect(action: Action) { + const payload = action.payload; + try { + const basePath = yield select(getBasePath); + const response = yield call(fetchMonitorLocations, { basePath, ...payload }); + yield put({ type: FETCH_MONITOR_LOCATIONS_SUCCESS, payload: response }); + } catch (error) { + yield put({ type: FETCH_MONITOR_LOCATIONS_FAIL, payload: error.message }); + } +} + export function* fetchMonitorDetailsEffect() { yield takeLatest(FETCH_MONITOR_DETAILS, monitorDetailsEffect); + yield takeLatest(FETCH_MONITOR_LOCATIONS, monitorLocationsEffect); } diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts index 4cacb6f8cab9e4..220ab0b2054622 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/monitor.ts @@ -10,16 +10,24 @@ import { FETCH_MONITOR_DETAILS, FETCH_MONITOR_DETAILS_SUCCESS, FETCH_MONITOR_DETAILS_FAIL, + FETCH_MONITOR_LOCATIONS, + FETCH_MONITOR_LOCATIONS_SUCCESS, + FETCH_MONITOR_LOCATIONS_FAIL, } from '../actions/monitor'; +import { MonitorLocations } from '../../../common/runtime_types'; + +type MonitorLocationsList = Map; export interface MonitorState { monitorDetailsList: MonitorDetailsState[]; + monitorLocationsList: MonitorLocationsList; loading: boolean; errors: any[]; } const initialState: MonitorState = { monitorDetailsList: [], + monitorLocationsList: new Map(), loading: false, errors: [], }; @@ -42,10 +50,27 @@ export function monitorReducer(state = initialState, action: MonitorActionTypes) loading: false, }; case FETCH_MONITOR_DETAILS_FAIL: - const error = action.payload; return { ...state, - errors: [...state.errors, error], + errors: [...state.errors, action.payload], + }; + case FETCH_MONITOR_LOCATIONS: + return { + ...state, + loading: true, + }; + case FETCH_MONITOR_LOCATIONS_SUCCESS: + const monLocations = state.monitorLocationsList; + monLocations.set(action.payload.monitorId, action.payload); + return { + ...state, + monitorLocationsList: monLocations, + loading: false, + }; + case FETCH_MONITOR_LOCATIONS_FAIL: + return { + ...state, + errors: [...state.errors, action.payload], }; default: return state; diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index 70cd2b19860baf..b61ed836634354 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -11,6 +11,7 @@ describe('state selectors', () => { const state: AppState = { monitor: { monitorDetailsList: [], + monitorLocationsList: new Map(), loading: false, errors: [], }, diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 245b45a9399505..1792c84c45220e 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -14,3 +14,7 @@ export const isIntegrationsPopupOpen = ({ ui: { integrationsPopoverOpen } }: App export const getMonitorDetails = (state: AppState, summary: any) => { return state.monitor.monitorDetailsList[summary.monitor_id]; }; + +export const getMonitorLocations = (state: AppState, monitorId: string) => { + return state.monitor.monitorLocationsList?.get(monitorId); +}; diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts index f6ac587b0ceec6..1191eb813a214e 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/adapter_types.ts @@ -17,4 +17,10 @@ export interface UMMonitorsAdapter { getFilterBar(request: any, dateRangeStart: string, dateRangeEnd: string): Promise; getMonitorPageTitle(request: any, monitorId: string): Promise; getMonitorDetails(request: any, monitorId: string): Promise; + getMonitorLocations( + request: any, + monitorId: string, + dateStart: string, + dateEnd: string + ): Promise; } diff --git a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts index c7ebc77af3567f..4009a4b1331fa3 100644 --- a/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts +++ b/x-pack/legacy/plugins/uptime/server/lib/adapters/monitors/elasticsearch_monitors_adapter.ts @@ -16,7 +16,12 @@ import { import { getHistogramIntervalFormatted } from '../../helper'; import { DatabaseAdapter } from '../database'; import { UMMonitorsAdapter } from './adapter_types'; -import { MonitorDetails, MonitorError } from '../../../../common/runtime_types'; +import { + MonitorDetails, + MonitorError, + MonitorLocations, + MonitorLocation, +} from '../../../../common/runtime_types'; const formatStatusBuckets = (time: any, buckets: any, docCount: any) => { let up = null; @@ -273,6 +278,11 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { }; } + /** + * Fetch data for the monitor page title. + * @param request Kibana server request + * @param monitorId the ID to query + */ public async getMonitorDetails(request: any, monitorId: string): Promise { const params = { index: INDEX_NAMES.HEARTBEAT, @@ -320,4 +330,100 @@ export class ElasticsearchMonitorsAdapter implements UMMonitorsAdapter { timestamp: errorTimeStamp, }; } + + /** + * Fetch data for the monitor page title. + * @param request Kibana server request + * @param monitorId the ID to query + */ + public async getMonitorLocations( + request: any, + monitorId: string, + dateStart: string, + dateEnd: string + ): Promise { + const params = { + index: INDEX_NAMES.HEARTBEAT, + body: { + size: 0, + query: { + bool: { + filter: [ + { + match: { + 'monitor.id': monitorId, + }, + }, + { + exists: { + field: 'summary', + }, + }, + { + range: { + '@timestamp': { + gte: dateStart, + lte: dateEnd, + }, + }, + }, + ], + }, + }, + aggs: { + location: { + terms: { + field: 'observer.geo.name', + missing: '__location_missing__', + }, + aggs: { + most_recent: { + top_hits: { + size: 1, + sort: { + '@timestamp': { + order: 'desc', + }, + }, + _source: ['monitor', 'summary', 'observer'], + }, + }, + }, + }, + }, + }, + }; + + const result = await this.database.search(request, params); + const locations = result?.aggregations?.location?.buckets ?? []; + + const getGeo = (locGeo: any) => { + const { name, location } = locGeo; + const latLon = location.trim().split(','); + return { + name, + location: { + lat: latLon[0], + lon: latLon[1], + }, + }; + }; + + const monLocs: MonitorLocation[] = []; + locations.forEach((loc: any) => { + if (loc?.key !== '__location_missing__') { + const mostRecentLocation = loc.most_recent.hits.hits[0]._source; + const location: MonitorLocation = { + summary: mostRecentLocation?.summary, + geo: getGeo(mostRecentLocation?.observer?.geo), + }; + monLocs.push(location); + } + }); + + return { + monitorId, + locations: monLocs, + }; + } } diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts index 2810982fb0c6c7..f18b9e8e44c36b 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/index.ts @@ -9,7 +9,7 @@ import { createGetIndexPatternRoute } from './index_pattern'; import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry'; import { createGetSnapshotCount } from './snapshot'; import { UMRestApiRouteCreator } from './types'; -import { createGetMonitorDetailsRoute } from './monitors'; +import { createGetMonitorDetailsRoute, createGetMonitorLocationsRoute } from './monitors'; export * from './types'; export { createRouteWithAuth } from './create_route_with_auth'; @@ -17,6 +17,7 @@ export const restApiRoutes: UMRestApiRouteCreator[] = [ createGetAllRoute, createGetIndexPatternRoute, createGetMonitorDetailsRoute, + createGetMonitorLocationsRoute, createGetSnapshotCount, createLogMonitorPageRoute, createLogOverviewPageRoute, diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts index 2c4b9e9fb1f3e1..2279233d49a096 100644 --- a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/index.ts @@ -5,3 +5,4 @@ */ export { createGetMonitorDetailsRoute } from './monitors_details'; +export { createGetMonitorLocationsRoute } from './monitor_locations'; diff --git a/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.ts new file mode 100644 index 00000000000000..4a91255bd19f2b --- /dev/null +++ b/x-pack/legacy/plugins/uptime/server/rest_api/monitors/monitor_locations.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 { schema } from '@kbn/config-schema'; +import { UMServerLibs } from '../../lib/lib'; +import { UMRestApiRouteCreator } from '../types'; + +export const createGetMonitorLocationsRoute: UMRestApiRouteCreator = (libs: UMServerLibs) => ({ + method: 'GET', + path: '/api/uptime/monitor/locations', + validate: { + query: schema.object({ + monitorId: schema.string(), + dateStart: schema.string(), + dateEnd: schema.string(), + }), + }, + options: { + tags: ['access:uptime'], + }, + handler: async (_context, request, response): Promise => { + const { monitorId, dateStart, dateEnd } = request.query; + + return response.ok({ + body: { + ...(await libs.monitors.getMonitorLocations(request, monitorId, dateStart, dateEnd)), + }, + }); + }, +}); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx new file mode 100644 index 00000000000000..de285ee15b59d9 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/app_context.mock.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ComponentType } from 'enzyme'; +import { + chromeServiceMock, + docLinksServiceMock, + uiSettingsServiceMock, + notificationServiceMock, + httpServiceMock, +} from '../../../../../../../src/core/public/mocks'; +import { AppContextProvider } from '../../../public/np_ready/application/app_context'; + +export const mockContextValue = { + docLinks: docLinksServiceMock.createStartContract(), + chrome: chromeServiceMock.createStartContract(), + legacy: { + TimeBuckets: class MockTimeBuckets { + setBounds(_domain: any) { + return {}; + } + getInterval() { + return { + expression: {}, + }; + } + }, + MANAGEMENT_BREADCRUMB: { text: 'test' }, + licenseStatus: {}, + }, + uiSettings: uiSettingsServiceMock.createSetupContract(), + toasts: notificationServiceMock.createSetupContract().toasts, + euiUtils: { + useChartsTheme: jest.fn(), + }, + // For our test harness, we don't use this mocked out http service + http: httpServiceMock.createSetupContract(), +}; + +export const withAppContext = (Component: ComponentType) => (props: any) => { + return ( + + + + ); +}; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts new file mode 100644 index 00000000000000..3b3df5fd6f8797 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/body_response.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const wrapBodyResponse = (obj: object) => JSON.stringify({ body: JSON.stringify(obj) }); + +export const unwrapBodyResponse = (string: string) => JSON.parse(JSON.parse(string).body); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts index 2170559dace5a3..7d9c1e4163d7b8 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/http_requests.ts @@ -34,7 +34,7 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => { const defaultResponse = { watchHistoryItems: [] }; server.respondWith( 'GET', - `${API_ROOT}/watch/:id/history?startTime=*`, + `${API_ROOT}/watch/:id/history`, mockResponse(defaultResponse, response) ); }; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts index ad005078db0a8e..814028fe599ff6 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/index.ts @@ -11,7 +11,7 @@ import { setup as watchCreateThresholdSetup } from './watch_create_threshold.hel import { setup as watchEditSetup } from './watch_edit.helpers'; export { nextTick, getRandomString, findTestSubject, TestBed } from '../../../../../../test_utils'; - +export { wrapBodyResponse, unwrapBodyResponse } from './body_response'; export { setupEnvironment } from './setup_environment'; export const pageHelpers = { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts index 806840a7821fd5..7e748073c1c6b2 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/setup_environment.ts @@ -7,9 +7,17 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import { init as initHttpRequests } from './http_requests'; -import { setHttpClient, setSavedObjectsClient } from '../../../public/lib/api'; +import { setHttpClient, setSavedObjectsClient } from '../../../public/np_ready/application/lib/api'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +mockHttpClient.interceptors.response.use( + res => { + return res.data; + }, + rej => { + return Promise.reject(rej); + } +); const mockSavedObjectsClient = () => { return { @@ -23,7 +31,7 @@ export const setupEnvironment = () => { // @ts-ignore setHttpClient(mockHttpClient); - setSavedObjectsClient(mockSavedObjectsClient()); + setSavedObjectsClient(mockSavedObjectsClient() as any); return { server, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts index bea215281a4bc9..dafcf3a7070d22 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_json.helpers.ts @@ -3,10 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { withAppContext } from './app_context.mock'; import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchCreateJsonTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts index e33ae02036224e..8cebe8ce262296 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_create_threshold.helpers.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES, WATCH_TYPES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -17,7 +18,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchCreateThresholdTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts index d0b458e30c70e2..187f4dcaa0a76f 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_edit.helpers.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { registerTestBed, TestBed, TestBedConfig } from '../../../../../../test_utils'; -import { WatchEdit } from '../../../public/sections/watch_edit/components/watch_edit'; +import { WatchEdit } from '../../../public/np_ready/application/sections/watch_edit/components/watch_edit'; import { ROUTES } from '../../../common/constants'; -import { registerRouter } from '../../../public/lib/navigation'; +import { registerRouter } from '../../../public/np_ready/application/lib/navigation'; import { WATCH_ID } from './constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -18,7 +19,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchEdit, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchEdit), testBedConfig); export interface WatchEditTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts index 0d3ecaa7a2b9a8..e33327ea42ffe8 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_list.helpers.ts @@ -13,8 +13,9 @@ import { TestBedConfig, nextTick, } from '../../../../../../test_utils'; -import { WatchList } from '../../../public/sections/watch_list/components/watch_list'; +import { WatchList } from '../../../public/np_ready/application/sections/watch_list/components/watch_list'; import { ROUTES } from '../../../common/constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -23,7 +24,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchList, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchList), testBedConfig); export interface WatchListTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts index 22d57f255ebe61..e7bffe8924e319 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/helpers/watch_status.helpers.ts @@ -13,9 +13,10 @@ import { TestBedConfig, nextTick, } from '../../../../../../test_utils'; -import { WatchStatus } from '../../../public/sections/watch_status/components/watch_status'; +import { WatchStatus } from '../../../public/np_ready/application/sections/watch_status/components/watch_status'; import { ROUTES } from '../../../common/constants'; import { WATCH_ID } from './constants'; +import { withAppContext } from './app_context.mock'; const testBedConfig: TestBedConfig = { memoryRouter: { @@ -25,7 +26,7 @@ const testBedConfig: TestBedConfig = { doMountAsync: true, }; -const initTestBed = registerTestBed(WatchStatus, testBedConfig); +const initTestBed = registerTestBed(withAppContext(WatchStatus), testBedConfig); export interface WatchStatusTestBed extends TestBed { actions: { diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts index f45dbe156723b4..4c893978ee5cbc 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_json.test.ts @@ -4,22 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import { act } from 'react-dom/test-utils'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchCreateJsonTestBed } from './helpers/watch_create_json.helpers'; import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/models/watch/default_watch.json'; +import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json'; import { getExecuteDetails } from '../../test/fixtures'; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchCreateJson; -describe.skip(' create route', () => { +describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateJsonTestBed; @@ -107,7 +100,7 @@ describe.skip(' create route', () => { 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id: watch.id, name: watch.name, type: watch.type, @@ -194,7 +187,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes, }), @@ -258,7 +251,7 @@ describe.skip(' create route', () => { const scheduledTime = `now+${SCHEDULED_TIME}s`; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ triggerData: { triggeredTime, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx index 62cfd92182091b..36a5c150eead73 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_create_threshold.test.tsx @@ -7,7 +7,13 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { + setupEnvironment, + pageHelpers, + nextTick, + wrapBodyResponse, + unwrapBodyResponse, +} from './helpers'; import { WatchCreateThresholdTestBed } from './helpers/watch_create_threshold.helpers'; import { getExecuteDetails } from '../../test/fixtures'; import { WATCH_TYPES } from '../../common/constants'; @@ -42,31 +48,8 @@ const WATCH_VISUALIZE_DATA = { const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', - getUiSettingsClient: () => ({ - get: () => {}, - isDefault: () => true, - }), -})); - -jest.mock('ui/time_buckets', () => { - class MockTimeBuckets { - setBounds(_domain: any) { - return {}; - } - getInterval() { - return { - expression: {}, - }; - } - } - return { TimeBuckets: MockTimeBuckets }; -}); - -jest.mock('../../public/lib/api', () => ({ - ...jest.requireActual('../../public/lib/api'), +jest.mock('../../public/np_ready/application/lib/api', () => ({ + ...jest.requireActual('../../public/np_ready/application/lib/api'), loadIndexPatterns: async () => { const INDEX_PATTERNS = [ { attributes: { title: 'index1' } }, @@ -85,7 +68,7 @@ jest.mock('@elastic/eui', () => ({ EuiComboBox: (props: any) => ( { + onChange={(syntheticEvent: any) => { props.onChange([syntheticEvent['0']]); }} /> @@ -94,7 +77,7 @@ jest.mock('@elastic/eui', () => ({ const { setup } = pageHelpers.watchCreateThreshold; -describe.skip(' create route', () => { +describe(' create route', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchCreateThresholdTestBed; @@ -105,12 +88,9 @@ describe.skip(' create route', () => { describe('on component mount', () => { beforeEach(async () => { testBed = await setup(); - - await act(async () => { - const { component } = testBed; - await nextTick(); - component.update(); - }); + const { component } = testBed; + await nextTick(); + component.update(); }); test('should set the correct page title', () => { @@ -125,13 +105,6 @@ describe.skip(' create route', () => { httpRequestsMockHelpers.setLoadEsFieldsResponse({ fields: ES_FIELDS }); httpRequestsMockHelpers.setLoadSettingsResponse(SETTINGS); httpRequestsMockHelpers.setLoadWatchVisualizeResponse(WATCH_VISUALIZE_DATA); - - testBed = await setup(); - - await act(async () => { - await nextTick(); - testBed.component.update(); - }); }); describe('form validation', () => { @@ -173,7 +146,7 @@ describe.skip(' create route', () => { expect(find('saveWatchButton').props().disabled).toEqual(true); }); - test('it should enable the Create button and render additonal content with valid fields', async () => { + test('it should enable the Create button and render additional content with valid fields', async () => { const { form, find, component, exists } = testBed; form.setInputValue('nameInput', 'my_test_watch'); @@ -192,39 +165,30 @@ describe.skip(' create route', () => { expect(exists('watchActionsPanel')).toBe(true); }); - describe('watch conditions', () => { - beforeEach(async () => { - const { form, find, component } = testBed; + // Looks like there is an issue with using 'mockComboBox'. + describe.skip('watch conditions', () => { + beforeEach(() => { + const { form, find } = testBed; // Name, index and time fields are required before the watch condition expression renders form.setInputValue('nameInput', 'my_test_watch'); - find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox - form.setInputValue('watchTimeFieldSelect', '@timestamp'); - - await act(async () => { - await nextTick(); - component.update(); + act(() => { + find('mockComboBox').simulate('change', [{ label: 'index1', value: 'index1' }]); // Using mocked EuiComboBox }); + form.setInputValue('watchTimeFieldSelect', '@timestamp'); }); - test('should require a threshold value', async () => { - const { form, find, component } = testBed; - - find('watchThresholdButton').simulate('click'); + test('should require a threshold value', () => { + const { form, find } = testBed; - // Provide invalid value - form.setInputValue('watchThresholdInput', ''); - - expect(form.getErrorsMessages()).toContain('A value is required.'); - - // Provide valid value - form.setInputValue('watchThresholdInput', '0'); - - await act(async () => { - await nextTick(); - component.update(); + act(() => { + find('watchThresholdButton').simulate('click'); + // Provide invalid value + form.setInputValue('watchThresholdInput', ''); + // Provide valid value + form.setInputValue('watchThresholdInput', '0'); }); - + expect(form.getErrorsMessages()).toContain('A value is required.'); expect(form.getErrorsMessages().length).toEqual(0); }); }); @@ -273,7 +237,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -300,7 +264,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { logging_1: 'force_execute', @@ -341,7 +305,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -367,7 +331,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { index_1: 'force_execute', @@ -401,7 +365,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -430,7 +394,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { slack_1: 'force_execute', @@ -471,7 +435,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -504,7 +468,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { email_1: 'force_execute', @@ -559,7 +523,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -594,7 +558,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { webhook_1: 'force_execute', @@ -645,7 +609,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -682,7 +646,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { jira_1: 'force_execute', @@ -723,7 +687,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).watch.id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).watch.id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -750,7 +714,7 @@ describe.skip(' create route', () => { }; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ executeDetails: getExecuteDetails({ actionModes: { pagerduty_1: 'force_execute', @@ -784,7 +748,7 @@ describe.skip(' create route', () => { const latestRequest = server.requests[server.requests.length - 1]; const thresholdWatch = { - id: JSON.parse(latestRequest.requestBody).id, // watch ID is created dynamically + id: unwrapBodyResponse(latestRequest.requestBody).id, // watch ID is created dynamically name: WATCH_NAME, type: WATCH_TYPES.THRESHOLD, isNew: true, @@ -801,7 +765,7 @@ describe.skip(' create route', () => { threshold: 1000, }; - expect(latestRequest.requestBody).toEqual(JSON.stringify(thresholdWatch)); + expect(latestRequest.requestBody).toEqual(wrapBodyResponse(thresholdWatch)); }); }); }); diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts index fb9ad934249e99..1eee3d3b7e6ee6 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_edit.test.ts @@ -6,36 +6,17 @@ import { act } from 'react-dom/test-utils'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; import axios from 'axios'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { setupEnvironment, pageHelpers, nextTick, wrapBodyResponse } from './helpers'; import { WatchEditTestBed } from './helpers/watch_edit.helpers'; import { WATCH } from './helpers/constants'; -import defaultWatchJson from '../../public/models/watch/default_watch.json'; +import defaultWatchJson from '../../public/np_ready/application/models/watch/default_watch.json'; import { getWatch } from '../../test/fixtures'; import { getRandomString } from '../../../../../test_utils'; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => { - class MockTimeBuckets { - setBounds(_domain: any) { - return {}; - } - getInterval() { - return { - expression: {}, - }; - } - } - return { TimeBuckets: MockTimeBuckets }; -}); - -jest.mock('../../public/lib/api', () => ({ - ...jest.requireActual('../../public/lib/api'), +jest.mock('../../public/np_ready/application/lib/api', () => ({ + ...jest.requireActual('../../public/np_ready/application/lib/api'), loadIndexPatterns: async () => { const INDEX_PATTERNS = [ { attributes: { title: 'index1' } }, @@ -49,7 +30,7 @@ jest.mock('../../public/lib/api', () => ({ const { setup } = pageHelpers.watchEdit; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchEditTestBed; @@ -110,7 +91,7 @@ describe.skip('', () => { 'There are {{ctx.payload.hits.total}} documents in your index. Threshold is 10.'; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id: watch.id, name: EDITED_WATCH_NAME, type: watch.type, @@ -202,7 +183,7 @@ describe.skip('', () => { } = watch; expect(latestRequest.requestBody).toEqual( - JSON.stringify({ + wrapBodyResponse({ id, name: EDITED_WATCH_NAME, type, diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts index bc2eadb7d9be99..a0327c6dfa1dbf 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_list.test.ts @@ -18,16 +18,9 @@ import { ROUTES } from '../../common/constants'; const { API_ROOT } = ROUTES; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchList; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchListTestBed; diff --git a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts index e12acd2e32ccf1..973c14893f3427 100644 --- a/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts +++ b/x-pack/legacy/plugins/watcher/__jest__/client_integration/watch_status.test.ts @@ -14,13 +14,6 @@ import { WATCH_STATES, ACTION_STATES } from '../../common/constants'; const { API_ROOT } = ROUTES; -jest.mock('ui/chrome', () => ({ - breadcrumbs: { set: () => {} }, - addBasePath: (path: string) => path || '/api/watcher', -})); - -jest.mock('ui/time_buckets', () => {}); - const { setup } = pageHelpers.watchStatus; const watchHistory1 = getWatchHistory({ startTime: '2019-06-04T01:11:11.294' }); @@ -45,7 +38,7 @@ const watch = { }, }; -describe.skip('', () => { +describe('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: WatchStatusTestBed; diff --git a/x-pack/legacy/plugins/watcher/kibana.json b/x-pack/legacy/plugins/watcher/kibana.json new file mode 100644 index 00000000000000..ccec8a1b776836 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "watcher", + "version": "kibana", + "requiredPlugins": [ + "home" + ], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/watcher/plugin_definition.js b/x-pack/legacy/plugins/watcher/plugin_definition.js deleted file mode 100644 index 4a5946cc4974d1..00000000000000 --- a/x-pack/legacy/plugins/watcher/plugin_definition.js +++ /dev/null @@ -1,46 +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 { i18n } from '@kbn/i18n'; -import { registerFieldsRoutes } from './server/routes/api/fields'; -import { registerSettingsRoutes } from './server/routes/api/settings'; -import { registerHistoryRoutes } from './server/routes/api/history'; -import { registerIndicesRoutes } from './server/routes/api/indices'; -import { registerLicenseRoutes } from './server/routes/api/license'; -import { registerWatchesRoutes } from './server/routes/api/watches'; -import { registerWatchRoutes } from './server/routes/api/watch'; -import { registerLicenseChecker } from '../../server/lib/register_license_checker'; -import { PLUGIN } from './common/constants'; - -export const pluginDefinition = { - id: PLUGIN.ID, - configPrefix: 'xpack.watcher', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/watcher'], - home: ['plugins/watcher/register_feature'], - }, - init: function (server) { - // Register license checker - registerLicenseChecker( - server, - PLUGIN.ID, - PLUGIN.getI18nName(i18n), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - - registerFieldsRoutes(server); - registerHistoryRoutes(server); - registerIndicesRoutes(server); - registerLicenseRoutes(server); - registerSettingsRoutes(server); - registerWatchesRoutes(server); - registerWatchRoutes(server); - }, -}; diff --git a/x-pack/legacy/plugins/watcher/plugin_definition.ts b/x-pack/legacy/plugins/watcher/plugin_definition.ts new file mode 100644 index 00000000000000..2da05253fdb325 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/plugin_definition.ts @@ -0,0 +1,32 @@ +/* + * 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 { plugin } from './server/np_ready'; +import { PLUGIN } from './common/constants'; + +export const pluginDefinition = { + id: PLUGIN.ID, + configPrefix: 'xpack.watcher', + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch', 'xpack_main'], + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/np_ready/application/index.scss'), + managementSections: ['plugins/watcher/legacy'], + home: ['plugins/watcher/register_feature'], + }, + init(server: any) { + plugin({} as any).setup(server.newPlatform.setup.core, { + __LEGACY: { + route: server.route.bind(server), + plugins: { + watcher: server.plugins[PLUGIN.ID], + xpack_main: server.plugins.xpack_main, + }, + }, + }); + }, +}; diff --git a/x-pack/legacy/plugins/watcher/public/app.html b/x-pack/legacy/plugins/watcher/public/app.html deleted file mode 100644 index 8c7c3eb946aefc..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/app.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
\ No newline at end of file diff --git a/x-pack/legacy/plugins/watcher/public/index.js b/x-pack/legacy/plugins/watcher/public/index.js deleted file mode 100644 index c1b84e76d0008b..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/index.js +++ /dev/null @@ -1,8 +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 './register_route'; -import './register_management_sections'; diff --git a/x-pack/legacy/plugins/watcher/public/legacy.ts b/x-pack/legacy/plugins/watcher/public/legacy.ts new file mode 100644 index 00000000000000..d7b85ccfeb7b42 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/legacy.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 { CoreSetup, App, AppUnmount } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +/* Legacy UI imports */ +import { npSetup, npStart } from 'ui/new_platform'; +import routes from 'ui/routes'; +import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; +import { TimeBuckets } from 'ui/time_buckets'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +/* Legacy UI imports */ + +import { plugin } from './np_ready'; +import { PLUGIN } from '../common/constants'; +import { LICENSE_STATUS_INVALID, LICENSE_STATUS_UNAVAILABLE } from '../../../common/constants'; +import { manageAngularLifecycle } from './manage_angular_lifecycle'; + +const template = ` +
+
`; + +let elem: HTMLElement; +let mountApp: () => AppUnmount | Promise; +let unmountApp: AppUnmount | Promise; +routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param4?', { + template, + controller: class WatcherController { + constructor($injector: any, $scope: any) { + const $route = $injector.get('$route'); + const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); + const shimCore: CoreSetup = { + ...npSetup.core, + application: { + ...npSetup.core.application, + register(app: App): void { + mountApp = () => + app.mount(npStart as any, { + element: elem, + appBasePath: '/management/elasticsearch/watcher/', + }); + }, + }, + }; + + // clean up previously rendered React app if one exists + // this happens because of React Router redirects + if (elem) { + ((unmountApp as unknown) as AppUnmount)(); + } + + $scope.$$postDigest(() => { + elem = document.getElementById('watchReactRoot')!; + const instance = plugin(); + instance.setup(shimCore, { + ...(npSetup.plugins as typeof npSetup.plugins & { eui_utils: any }), + __LEGACY: { + MANAGEMENT_BREADCRUMB, + TimeBuckets, + licenseStatus, + }, + }); + + instance.start(npStart.core, npStart.plugins); + + (mountApp() as Promise).then(fn => (unmountApp = fn)); + + manageAngularLifecycle($scope, $route, elem); + }); + } + } as any, + // @ts-ignore + controllerAs: 'watchRoute', +}); + +routes.defaults(/\/management/, { + resolve: { + watcherManagementSection: () => { + const watchesSection = management.getSection('elasticsearch/watcher'); + const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); + const { status } = licenseStatus; + + if (status === LICENSE_STATUS_INVALID || status === LICENSE_STATUS_UNAVAILABLE) { + return watchesSection.hide(); + } + + watchesSection.show(); + }, + }, +}); + +management.getSection('elasticsearch').register('watcher', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watcherDisplayName', { + defaultMessage: 'Watcher', + }), + order: 6, + url: '#/management/elasticsearch/watcher/', +} as any); + +management.getSection('elasticsearch/watcher').register('watches', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watchesDisplayName', { + defaultMessage: 'Watches', + }), + order: 1, +} as any); + +management.getSection('elasticsearch/watcher').register('watch', { + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('status', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.statusDisplayName', { + defaultMessage: 'Status', + }), + order: 1, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('edit', { + display: i18n.translate('xpack.watcher.sections.watchList.managementSection.editDisplayName', { + defaultMessage: 'Edit', + }), + order: 2, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('new', { + display: i18n.translate( + 'xpack.watcher.sections.watchList.managementSection.newWatchDisplayName', + { + defaultMessage: 'New Watch', + } + ), + order: 1, + visible: false, +} as any); + +management.getSection('elasticsearch/watcher/watch').register('history-item', { + order: 1, + visible: false, +} as any); diff --git a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts b/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts deleted file mode 100644 index 88f23465d33e8d..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/lib/documentation_links/documentation_links.ts +++ /dev/null @@ -1,24 +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 { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; -import { ACTION_TYPES } from '../../../common/constants'; - -const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`; - -const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`; -const esStackBase = `${elasticDocLinkBase}elastic-stack-overview/${DOC_LINK_VERSION}`; -const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`; - -export const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`; -export const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`; -export const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`; - -export const watchActionsConfigurationMap = { - [ACTION_TYPES.SLACK]: `${esStackBase}/actions-slack.html#configuring-slack`, - [ACTION_TYPES.PAGERDUTY]: `${esStackBase}/actions-pagerduty.html#configuring-pagerduty`, - [ACTION_TYPES.JIRA]: `${esStackBase}/actions-jira.html#configuring-jira`, -}; diff --git a/x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js b/x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts similarity index 75% rename from x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js rename to x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts index 3813e632a0a738..efd40eaf83daad 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/manage_angular_lifecycle.js +++ b/x-pack/legacy/plugins/watcher/public/manage_angular_lifecycle.ts @@ -6,7 +6,7 @@ import { unmountComponentAtNode } from 'react-dom'; -export const manageAngularLifecycle = ($scope, $route, elem) => { +export const manageAngularLifecycle = ($scope: any, $route: any, elem: HTMLElement) => { const lastRoute = $route.current; const deregister = $scope.$on('$locationChangeSuccess', () => { @@ -17,7 +17,12 @@ export const manageAngularLifecycle = ($scope, $route, elem) => { }); $scope.$on('$destroy', () => { - deregister && deregister(); - elem && unmountComponentAtNode(elem); + if (deregister) { + deregister(); + } + + if (elem) { + unmountComponentAtNode(elem); + } }); }; diff --git a/x-pack/legacy/plugins/watcher/public/models/index.d.ts b/x-pack/legacy/plugins/watcher/public/models/index.d.ts deleted file mode 100644 index d96d8d192e166e..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/models/index.d.ts +++ /dev/null @@ -1,42 +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. - */ -declare module 'plugins/watcher/models/visualize_options' { - export const VisualizeOptions: any; -} - -declare module 'plugins/watcher/models/watch' { - export const Watch: any; -} - -declare module 'plugins/watcher/models/watch/threshold_watch' { - export const ThresholdWatch: any; -} - -declare module 'plugins/watcher/models/watch/json_watch' { - export const JsonWatch: any; -} - -declare module 'plugins/watcher/models/execute_details/execute_details' { - export const ExecuteDetails: any; -} - -declare module 'plugins/watcher/models/watch_history_item' { - export const WatchHistoryItem: any; -} - -declare module 'plugins/watcher/models/watch_status' { - export const WatchStatus: any; -} - -declare module 'plugins/watcher/models/settings' { - export const Settings: any; -} -declare module 'plugins/watcher/models/action' { - export const Action: any; -} -declare module 'ui/time_buckets' { - export const TimeBuckets: any; -} diff --git a/x-pack/legacy/plugins/watcher/public/app.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx similarity index 60% rename from x-pack/legacy/plugins/watcher/public/app.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx index b206348547966d..36fa1cce9d6ddc 100644 --- a/x-pack/legacy/plugins/watcher/public/app.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/app.tsx @@ -4,54 +4,61 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { HashRouter, Switch, Route, Redirect } from 'react-router-dom'; +import React from 'react'; +import { + ChromeStart, + DocLinksStart, + HttpSetup, + ToastsSetup, + IUiSettingsClient, +} from 'src/core/public'; + +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { + HashRouter, + Switch, + Route, + Redirect, + withRouter, + RouteComponentProps, +} from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { WatchStatus } from './sections/watch_status/components/watch_status'; import { WatchEdit } from './sections/watch_edit/components/watch_edit'; import { WatchList } from './sections/watch_list/components/watch_list'; import { registerRouter } from './lib/navigation'; import { BASE_PATH } from './constants'; -import { LICENSE_STATUS_VALID } from '../../../common/constants'; -import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { LICENSE_STATUS_VALID } from '../../../../../common/constants'; +import { AppContextProvider } from './app_context'; +import { LegacyDependencies } from '../types'; -class ShareRouter extends Component { - static contextTypes = { - router: PropTypes.shape({ - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired - }).isRequired - }).isRequired - } - constructor(...args) { - super(...args); - this.registerRouter(); - } +const ShareRouter = withRouter(({ children, history }: RouteComponentProps & { children: any }) => { + registerRouter({ history }); + return children; +}); - registerRouter() { - // Share the router with the app without requiring React or context. - const { router } = this.context; - registerRouter(router); - } - - render() { - return this.props.children; - } +export interface AppDeps { + chrome: ChromeStart; + docLinks: DocLinksStart; + toasts: ToastsSetup; + http: HttpSetup; + uiSettings: IUiSettingsClient; + legacy: LegacyDependencies; + euiUtils: any; } -export const App = ({ licenseStatus }) => { - const { status, message } = licenseStatus; + +export const App = (deps: AppDeps) => { + const { status, message } = deps.legacy.licenseStatus; if (status !== LICENSE_STATUS_VALID) { return ( - )} + } color="warning" iconType="help" > @@ -69,7 +76,9 @@ export const App = ({ licenseStatus }) => { return ( - + + + ); @@ -81,7 +90,11 @@ export const AppWithoutRouter = () => ( - + ); diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx new file mode 100644 index 00000000000000..5696ab3cb91ba1 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/app_context.tsx @@ -0,0 +1,65 @@ +/* + * 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, { createContext, useContext } from 'react'; +import { DocLinksStart } from 'src/core/public'; +import { ACTION_TYPES } from '../../../common/constants'; +import { AppDeps } from './app'; + +interface ContextValue extends Omit { + links: ReturnType; +} + +const AppContext = createContext(null as any); + +const generateDocLinks = ({ ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }: DocLinksStart) => { + const elasticDocLinkBase = `${ELASTIC_WEBSITE_URL}guide/en/`; + const esBase = `${elasticDocLinkBase}elasticsearch/reference/${DOC_LINK_VERSION}`; + const kibanaBase = `${elasticDocLinkBase}kibana/${DOC_LINK_VERSION}`; + const putWatchApiUrl = `${esBase}/watcher-api-put-watch.html`; + const executeWatchApiUrl = `${esBase}/watcher-api-execute-watch.html#watcher-api-execute-watch-action-mode`; + const watcherGettingStartedUrl = `${kibanaBase}/watcher-ui.html`; + const watchActionsConfigurationMap = { + [ACTION_TYPES.SLACK]: `${esBase}/actions-slack.html#configuring-slack`, + [ACTION_TYPES.PAGERDUTY]: `${esBase}/actions-pagerduty.html#configuring-pagerduty`, + [ACTION_TYPES.JIRA]: `${esBase}/actions-jira.html#configuring-jira`, + }; + + return { + putWatchApiUrl, + executeWatchApiUrl, + watcherGettingStartedUrl, + watchActionsConfigurationMap, + }; +}; + +export const AppContextProvider = ({ + children, + value, +}: { + value: AppDeps; + children: React.ReactNode; +}) => { + const { docLinks, ...rest } = value; + return ( + + {children} + + ); +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('"useAppContext" can only be called inside of AppContext.Provider!'); + } + return ctx; +}; diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx new file mode 100644 index 00000000000000..3f2a10f0046495 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/boot.tsx @@ -0,0 +1,35 @@ +/* + * 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 { render, unmountComponentAtNode } from 'react-dom'; +import { SavedObjectsClientContract } from 'src/core/public'; + +import { App, AppDeps } from './app'; +import { setHttpClient, setSavedObjectsClient } from './lib/api'; +import { LegacyDependencies } from '../types'; + +interface BootDeps extends AppDeps { + element: HTMLElement; + savedObjects: SavedObjectsClientContract; + I18nContext: any; + legacy: LegacyDependencies; +} + +export const boot = (bootDeps: BootDeps) => { + const { I18nContext, element, legacy, savedObjects, ...appDeps } = bootDeps; + + setHttpClient(appDeps.http); + setSavedObjectsClient(savedObjects); + + render( + + + , + element + ); + return () => unmountComponentAtNode(element); +}; diff --git a/x-pack/legacy/plugins/watcher/public/components/confirm_watches_modal.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/confirm_watches_modal.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/confirm_watches_modal.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/confirm_watches_modal.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx index 6d75495cbfc20d..363185f3457d80 100644 --- a/x-pack/legacy/plugins/watcher/public/components/delete_watches_modal.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/delete_watches_modal.tsx @@ -6,8 +6,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { toastNotifications } from 'ui/notify'; import { deleteWatches } from '../lib/api'; +import { useAppContext } from '../app_context'; export const DeleteWatchesModal = ({ watchesToDelete, @@ -16,6 +16,7 @@ export const DeleteWatchesModal = ({ watchesToDelete: string[]; callback: (deleted?: string[]) => void; }) => { + const { toasts } = useAppContext(); const numWatchesToDelete = watchesToDelete.length; if (!numWatchesToDelete) { return null; @@ -54,7 +55,7 @@ export const DeleteWatchesModal = ({ const numErrors = errors.length; callback(successes); if (numSuccesses > 0) { - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate( 'xpack.watcher.sections.watchList.deleteSelectedWatchesSuccessNotification.descriptionText', { @@ -67,7 +68,7 @@ export const DeleteWatchesModal = ({ } if (numErrors > 0) { - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.watcher.sections.watchList.deleteSelectedWatchesErrorNotification.descriptionText', { diff --git a/x-pack/legacy/plugins/watcher/public/components/form_errors.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/form_errors.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/form_errors.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/form_errors.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error_forbidden.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_forbidden.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error_forbidden.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_forbidden.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/page_error/page_error_not_exist.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_not_exist.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/page_error/page_error_not_exist.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/page_error/page_error_not_exist.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/section_error.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx similarity index 80% rename from x-pack/legacy/plugins/watcher/public/components/section_error.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx index 8951b95b750781..1c77cf2b49ae26 100644 --- a/x-pack/legacy/plugins/watcher/public/components/section_error.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_error.tsx @@ -8,6 +8,18 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { Fragment } from 'react'; export interface Error { + error: string; + + /** + * wrapEsError() on the server adds a "cause" array + */ + cause?: string[]; + + message?: string; + + /** + * @deprecated + */ data: { error: string; cause?: string[]; @@ -21,11 +33,9 @@ interface Props { } export const SectionError: React.FunctionComponent = ({ title, error, ...rest }) => { - const { - error: errorString, - cause, // wrapEsError() on the server adds a "cause" array - message, - } = error.data; + const data = error.data || error; + + const { error: errorString, cause, message } = data; return ( diff --git a/x-pack/legacy/plugins/watcher/public/components/section_loading.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_loading.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/components/section_loading.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/section_loading.tsx diff --git a/x-pack/legacy/plugins/watcher/public/components/watch_status.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/components/watch_status.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx index 39e6a5247b4a6f..8afd174f8561e6 100644 --- a/x-pack/legacy/plugins/watcher/public/components/watch_status.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/components/watch_status.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { ACTION_STATES, WATCH_STATES } from '../../common/constants'; +import { ACTION_STATES, WATCH_STATES } from '../../../../common/constants'; function StatusIcon({ status }: { status: string }) { switch (status) { diff --git a/x-pack/legacy/plugins/watcher/public/constants/base_path.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/constants/base_path.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/constants/base_path.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/constants/base_path.ts diff --git a/x-pack/legacy/plugins/watcher/public/constants/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/constants/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/constants/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/index.scss b/x-pack/legacy/plugins/watcher/public/np_ready/application/index.scss similarity index 100% rename from x-pack/legacy/plugins/watcher/public/index.scss rename to x-pack/legacy/plugins/watcher/public/np_ready/application/index.scss diff --git a/x-pack/legacy/plugins/watcher/public/lib/api.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/public/lib/api.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts index d5c430f9244c41..c08545904e3516 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/api.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/api.ts @@ -3,20 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Settings } from 'plugins/watcher/models/settings'; -import { Watch } from 'plugins/watcher/models/watch'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { WatchStatus } from 'plugins/watcher/models/watch_status'; - -import { __await } from 'tslib'; -import chrome from 'ui/chrome'; -import { ROUTES } from '../../common/constants'; -import { BaseWatch, ExecutedWatchDetails } from '../../common/types/watch_types'; +import { HttpSetup, SavedObjectsClientContract } from 'src/core/public'; +import { Settings } from 'plugins/watcher/np_ready/application/models/settings'; +import { Watch } from 'plugins/watcher/np_ready/application/models/watch'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { WatchStatus } from 'plugins/watcher/np_ready/application/models/watch_status'; + +import { BaseWatch, ExecutedWatchDetails } from '../../../../common/types/watch_types'; import { useRequest, sendRequest } from './use_request'; -let httpClient: ng.IHttpService; +import { ROUTES } from '../../../../common/constants'; + +let httpClient: HttpSetup; -export const setHttpClient = (anHttpClient: ng.IHttpService) => { +export const setHttpClient = (anHttpClient: HttpSetup) => { httpClient = anHttpClient; }; @@ -24,19 +24,17 @@ export const getHttpClient = () => { return httpClient; }; -let savedObjectsClient: any; +let savedObjectsClient: SavedObjectsClientContract; -export const setSavedObjectsClient = (aSavedObjectsClient: any) => { +export const setSavedObjectsClient = (aSavedObjectsClient: SavedObjectsClientContract) => { savedObjectsClient = aSavedObjectsClient; }; -export const getSavedObjectsClient = () => { - return savedObjectsClient; -}; +export const getSavedObjectsClient = () => savedObjectsClient; -const basePath = chrome.addBasePath(ROUTES.API_ROOT); +const basePath = ROUTES.API_ROOT; -export const loadWatches = (pollIntervalMs: number) => { +export const useLoadWatches = (pollIntervalMs: number) => { return useRequest({ path: `${basePath}/watches`, method: 'get', @@ -47,7 +45,7 @@ export const loadWatches = (pollIntervalMs: number) => { }); }; -export const loadWatchDetail = (id: string) => { +export const useLoadWatchDetail = (id: string) => { return useRequest({ path: `${basePath}/watch/${id}`, method: 'get', @@ -55,15 +53,10 @@ export const loadWatchDetail = (id: string) => { }); }; -export const loadWatchHistory = (id: string, startTime: string) => { - let path = `${basePath}/watch/${id}/history`; - - if (startTime) { - path += `?startTime=${startTime}`; - } - +export const useLoadWatchHistory = (id: string, startTime: string) => { return useRequest({ - path, + query: startTime ? { startTime } : undefined, + path: `${basePath}/watch/${id}/history`, method: 'get', deserializer: ({ watchHistoryItems = [] }: { watchHistoryItems: any }) => { return watchHistoryItems.map((historyItem: any) => @@ -73,7 +66,7 @@ export const loadWatchHistory = (id: string, startTime: string) => { }); }; -export const loadWatchHistoryDetail = (id: string | undefined) => { +export const useLoadWatchHistoryDetail = (id: string | undefined) => { return useRequest({ path: !id ? '' : `${basePath}/history/${id}`, method: 'get', @@ -83,12 +76,10 @@ export const loadWatchHistoryDetail = (id: string | undefined) => { }; export const deleteWatches = async (watchIds: string[]) => { - const body = { + const body = JSON.stringify({ watchIds, - }; - const { - data: { results }, - } = await getHttpClient().post(`${basePath}/watches/delete`, body); + }); + const { results } = await getHttpClient().post(`${basePath}/watches/delete`, { body }); return results; }; @@ -107,8 +98,8 @@ export const activateWatch = async (id: string) => { }; export const loadWatch = async (id: string) => { - const { data: watch } = await getHttpClient().get(`${basePath}/watch/${id}`); - return Watch.fromUpstreamJson(watch.watch); + const { watch } = await getHttpClient().get(`${basePath}/watch/${id}`); + return Watch.fromUpstreamJson(watch); }; export const getMatchingIndices = async (pattern: string) => { @@ -118,32 +109,32 @@ export const getMatchingIndices = async (pattern: string) => { if (!pattern.endsWith('*')) { pattern = `${pattern}*`; } - const { - data: { indices }, - } = await getHttpClient().post(`${basePath}/indices`, { pattern }); + const body = JSON.stringify({ pattern }); + const { indices } = await getHttpClient().post(`${basePath}/indices`, { body }); return indices; }; export const fetchFields = async (indexes: string[]) => { - const { - data: { fields }, - } = await getHttpClient().post(`${basePath}/fields`, { indexes }); + const { fields } = await getHttpClient().post(`${basePath}/fields`, { + body: JSON.stringify({ indexes }), + }); return fields; }; export const createWatch = async (watch: BaseWatch) => { - const { data } = await getHttpClient().put(`${basePath}/watch/${watch.id}`, watch.upstreamJson); - return data; + return await getHttpClient().put(`${basePath}/watch/${watch.id}`, { + body: JSON.stringify(watch.upstreamJson), + }); }; export const executeWatch = async (executeWatchDetails: ExecutedWatchDetails, watch: BaseWatch) => { return sendRequest({ path: `${basePath}/watch/execute`, method: 'put', - body: { + body: JSON.stringify({ executeDetails: executeWatchDetails.upstreamJson, watch: watch.upstreamJson, - }, + }), }); }; @@ -156,19 +147,19 @@ export const loadIndexPatterns = async () => { return savedObjects; }; -export const getWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { +export const useGetWatchVisualizationData = (watchModel: BaseWatch, visualizeOptions: any) => { return useRequest({ path: `${basePath}/watch/visualize`, method: 'post', - body: { + body: JSON.stringify({ watch: watchModel.upstreamJson, options: visualizeOptions.upstreamJson, - }, + }), deserializer: ({ visualizeData }: { visualizeData: any }) => visualizeData, }); }; -export const loadSettings = () => { +export const useLoadSettings = () => { return useRequest({ path: `${basePath}/settings`, method: 'get', @@ -183,11 +174,8 @@ export const loadSettings = () => { }; export const ackWatchAction = async (watchId: string, actionId: string) => { - const { - data: { watchStatus }, - } = await getHttpClient().put( - `${basePath}/watch/${watchId}/action/${actionId}/acknowledge`, - null + const { watchStatus } = await getHttpClient().put( + `${basePath}/watch/${watchId}/action/${actionId}/acknowledge` ); return WatchStatus.fromUpstreamJson(watchStatus); }; diff --git a/x-pack/legacy/plugins/watcher/public/lib/breadcrumbs.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/breadcrumbs.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/breadcrumbs.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/breadcrumbs.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/format_date.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/format_date.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/format_date.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/format_date.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/get_search_value.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_search_value.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/get_search_value.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_search_value.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts similarity index 95% rename from x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts index 35bd19e7007c6b..ce3b96ac17def3 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/get_time_unit_label.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/get_time_unit_label.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { TIME_UNITS } from '../../common/constants'; +import { TIME_UNITS } from '../../../../common/constants'; export function getTimeUnitLabel(timeUnit = TIME_UNITS.SECOND, timeValue = '0') { switch (timeUnit) { diff --git a/x-pack/legacy/plugins/watcher/public/lib/navigation.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/navigation.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/lib/navigation.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/navigation.ts diff --git a/x-pack/legacy/plugins/watcher/public/lib/use_request.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts similarity index 99% rename from x-pack/legacy/plugins/watcher/public/lib/use_request.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts index 4788b655d9e881..572403b14b9df7 100644 --- a/x-pack/legacy/plugins/watcher/public/lib/use_request.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/lib/use_request.ts @@ -11,6 +11,7 @@ import { sendRequest as _sendRequest, useRequest as _useRequest, } from '../shared_imports'; + import { getHttpClient } from './api'; export const sendRequest = (config: SendRequestConfig): Promise => { diff --git a/x-pack/legacy/plugins/watcher/public/models/action/action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js similarity index 95% rename from x-pack/legacy/plugins/watcher/public/models/action/action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js index 2f1850c3a434cb..4e6ec21703b961 100644 --- a/x-pack/legacy/plugins/watcher/public/models/action/action.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/action.js @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { ACTION_TYPES } from '../../../common/constants'; +import { ACTION_TYPES } from '../../../../../common/constants'; import { EmailAction } from './email_action'; import { LoggingAction } from './logging_action'; import { SlackAction } from './slack_action'; diff --git a/x-pack/legacy/plugins/watcher/public/models/action/base_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/base_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/base_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/base_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/email_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/email_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/email_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/email_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/index_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js similarity index 98% rename from x-pack/legacy/plugins/watcher/public/models/action/index_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js index 7276ef59a3fc35..de84951cc3d276 100644 --- a/x-pack/legacy/plugins/watcher/public/models/action/index_action.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/index_action.js @@ -35,7 +35,6 @@ export class IndexAction extends BaseAction { const result = super.upstreamJson; Object.assign(result, { - index: this.index, index: { index: this.index, } diff --git a/x-pack/legacy/plugins/watcher/public/models/action/jira_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/jira_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/jira_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/jira_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/logging_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/logging_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/logging_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/logging_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/pagerduty_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/pagerduty_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/pagerduty_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/pagerduty_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/slack_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/slack_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/slack_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/slack_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/unknown_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/unknown_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/unknown_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/unknown_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action/webhook_action.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action/webhook_action.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action/webhook_action.js diff --git a/x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js similarity index 95% rename from x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js index fa9e056554ab00..b177eb5bb22914 100644 --- a/x-pack/legacy/plugins/watcher/public/models/action_status/action_status.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/action_status.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; export class ActionStatus { constructor(props = {}) { diff --git a/x-pack/legacy/plugins/watcher/public/models/action_status/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/action_status/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/action_status/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/execute_details/execute_details.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/execute_details/execute_details.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/public/models/execute_details/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/execute_details/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/execute_details/index.js diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts new file mode 100644 index 00000000000000..a8ddb6ca2b76db --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/index.d.ts @@ -0,0 +1,39 @@ +/* + * 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. + */ +declare module 'plugins/watcher/np_ready/application/models/visualize_options' { + export const VisualizeOptions: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch' { + export const Watch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch/threshold_watch' { + export const ThresholdWatch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch/json_watch' { + export const JsonWatch: any; +} + +declare module 'plugins/watcher/np_ready/application/models/execute_details/execute_details' { + export const ExecuteDetails: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch_history_item' { + export const WatchHistoryItem: any; +} + +declare module 'plugins/watcher/np_ready/application/models/watch_status' { + export const WatchStatus: any; +} + +declare module 'plugins/watcher/np_ready/application/models/settings' { + export const Settings: any; +} +declare module 'plugins/watcher/np_ready/application/models/action' { + export const Action: any; +} diff --git a/x-pack/legacy/plugins/watcher/public/models/settings/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/settings/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/settings/settings.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/settings.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/settings/settings.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/settings/settings.js diff --git a/x-pack/legacy/plugins/watcher/public/models/visualize_options/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/visualize_options/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/visualize_options/visualize_options.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/visualize_options.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/visualize_options/visualize_options.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/visualize_options/visualize_options.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts similarity index 94% rename from x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts index 65ab537889ea44..cefaaa3b1abd3a 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/agg_types.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/agg_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AGG_TYPES } from '../../../common/constants'; +import { AGG_TYPES } from '../../../../../common/constants'; export interface AggType { text: string; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/base_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/base_watch.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/base_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/base_watch.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts similarity index 96% rename from x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts index b636cdaf14c180..edc3a03c252271 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/comparators.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/comparators.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { COMPARATORS } from '../../../common/constants'; +import { COMPARATORS } from '../../../../../common/constants'; export interface Comparator { text: string; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/default_watch.json b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/default_watch.json similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/default_watch.json rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/default_watch.json diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/group_by_types.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/group_by_types.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/group_by_types.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/group_by_types.ts diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js similarity index 98% rename from x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js index 3dd7af759970e5..2e2ee47640cf08 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/json_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/json_watch.js @@ -7,7 +7,7 @@ import uuid from 'uuid'; import { get } from 'lodash'; import { BaseWatch } from './base_watch'; -import { ACTION_TYPES, WATCH_TYPES } from '../../../common/constants'; +import { ACTION_TYPES, WATCH_TYPES } from '../../../../../common/constants'; import defaultWatchJson from './default_watch.json'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/check_action_id_collision.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/check_action_id_collision.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/check_action_id_collision.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/check_action_id_collision.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/check_action_id_collision/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/check_action_id_collision/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/create_action_id.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/create_action_id.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/create_action_id.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/create_action_id.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch/lib/create_action_id/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/lib/create_action_id/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js similarity index 92% rename from x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js index a0873934e17591..3269fcbe459d28 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/monitoring_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/monitoring_watch.js @@ -5,7 +5,7 @@ */ import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../../common/constants'; /** * {@code MonitoringWatch} system defined watches created by the Monitoring plugin. diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js similarity index 99% rename from x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js index af995d6594a38a..02fa99e7f3e16c 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/threshold_watch.js @@ -6,7 +6,7 @@ import { BaseWatch } from './base_watch'; import uuid from 'uuid'; -import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../common/constants'; +import { WATCH_TYPES, SORT_ORDERS, COMPARATORS } from '../../../../../common/constants'; import { getTimeUnitLabel } from '../../lib/get_time_unit_label'; import { i18n } from '@kbn/i18n'; import { aggTypes } from './agg_types'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch/watch.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js similarity index 93% rename from x-pack/legacy/plugins/watcher/public/models/watch/watch.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js index d58a7799c65169..2723fed9206751 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch/watch.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch/watch.js @@ -5,7 +5,7 @@ */ import { get, set } from 'lodash'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../../common/constants'; import { JsonWatch } from './json_watch'; import { ThresholdWatch } from './threshold_watch'; import { MonitoringWatch } from './monitoring_watch'; diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_errors/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_errors/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_errors/watch_errors.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/watch_errors.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_errors/watch_errors.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_errors/watch_errors.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_history_item/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js similarity index 91% rename from x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js index a5918cec2764b5..785f9d19b23dd9 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch_history_item/watch_history_item.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_history_item/watch_history_item.js @@ -6,7 +6,7 @@ import 'moment-duration-format'; import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; import { WatchStatus } from '../watch_status'; export class WatchHistoryItem { diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_status/index.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/public/models/watch_status/index.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/index.js diff --git a/x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js similarity index 94% rename from x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js rename to x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js index f213032a93c27e..77007ea1903862 100644 --- a/x-pack/legacy/plugins/watcher/public/models/watch_status/watch_status.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/models/watch_status/watch_status.js @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../../common/lib/get_moment'; import { ActionStatus } from '../action_status'; export class WatchStatus { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx similarity index 92% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx index 9c4b16e301b384..010e430c0719a1 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit.tsx @@ -16,10 +16,10 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { getActionType } from '../../../../../common/lib/get_action_type'; -import { BaseWatch, ExecutedWatchDetails } from '../../../../../common/types/watch_types'; -import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { getActionType } from '../../../../../../../common/lib/get_action_type'; +import { BaseWatch, ExecutedWatchDetails } from '../../../../../../../common/types/watch_types'; +import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants'; import { JsonWatchEditForm } from './json_watch_edit_form'; import { JsonWatchEditSimulate } from './json_watch_edit_simulate'; import { WatchContext } from '../../watch_context'; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx index 02a54fc9b92792..376aeb205b855c 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_form.tsx @@ -20,15 +20,20 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { serializeJsonWatch } from '../../../../../common/lib/serialization'; -import { ErrableFormRow, SectionError } from '../../../../components'; -import { putWatchApiUrl } from '../../../../lib/documentation_links'; +import { serializeJsonWatch } from '../../../../../../../common/lib/serialization'; +import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; import { onWatchSave } from '../../watch_edit_actions'; import { WatchContext } from '../../watch_context'; import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; +import { useAppContext } from '../../../../app_context'; export const JsonWatchEditForm = () => { + const { + links: { putWatchApiUrl }, + toasts, + } = useAppContext(); + const { watch, setWatchProperty } = useContext(WatchContext); const { errors } = watch.validate(); @@ -37,9 +42,7 @@ export const JsonWatchEditForm = () => { const [validationError, setValidationError] = useState(null); const [isRequestVisible, setIsRequestVisible] = useState(false); - const [serverError, setServerError] = useState<{ - data: { nessage: string; error: string }; - } | null>(null); + const [serverError, setServerError] = useState(null); const [isSaving, setIsSaving] = useState(false); @@ -192,7 +195,7 @@ export const JsonWatchEditForm = () => { isDisabled={hasErrors} onClick={async () => { setIsSaving(true); - const savedWatch = await onWatchSave(watch); + const savedWatch = await onWatchSave(watch, toasts); if (savedWatch && savedWatch.error) { const { data } = savedWatch.error; setIsSaving(false); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx index e57a875aa4356d..7c5de3d8e92989 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate.tsx @@ -24,19 +24,19 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { ACTION_MODES, TIME_UNITS } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { ACTION_MODES, TIME_UNITS } from '../../../../../../../common/constants'; import { ExecutedWatchDetails, ExecutedWatchResults, -} from '../../../../../common/types/watch_types'; +} from '../../../../../../../common/types/watch_types'; import { ErrableFormRow } from '../../../../components/form_errors'; import { executeWatch } from '../../../../lib/api'; -import { executeWatchApiUrl } from '../../../../lib/documentation_links'; import { WatchContext } from '../../watch_context'; import { JsonWatchEditSimulateResults } from './json_watch_edit_simulate_results'; import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; +import { useAppContext } from '../../../../app_context'; const actionModeOptions = Object.keys(ACTION_MODES).map(mode => ({ text: ACTION_MODES[mode], @@ -70,6 +70,9 @@ export const JsonWatchEditSimulate = ({ type: string; }>; }) => { + const { + links: { executeWatchApiUrl }, + } = useAppContext(); const { watch } = useContext(WatchContext); // hooks diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx similarity index 99% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx index 1b2b4ab935e8c1..4b630f5bc81b46 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/json_watch_edit/json_watch_edit_simulate_results.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ExecutedWatchDetails, ExecutedWatchResults, -} from '../../../../../common/types/watch_types'; +} from '../../../../../../../common/types/watch_types'; import { getTypeFromAction } from '../../watch_edit_actions'; import { WatchContext } from '../../watch_context'; import { WatchStatus, SectionError } from '../../../../components'; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/monitoring_watch_edit/monitoring_watch_edit.tsx diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/request_flyout.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/request_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/request_flyout.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/request_flyout.tsx diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx index aebe8baaee4174..3e70e49f423504 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/email_action_fields.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiComboBox, EuiFieldText, EuiFormRow, EuiTextArea } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { EmailAction } from '../../../../../../common/types/action_types'; +import { EmailAction } from '../../../../../../../../common/types/action_types'; interface Props { action: EmailAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx index 1cafb08ca40602..b7ab76d9890bcf 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/index_action_fields.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { IndexAction } from '../../../../../../common/types/action_types'; +import { IndexAction } from '../../../../../../../../common/types/action_types'; interface Props { action: IndexAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx index b8bdeaff908216..c09b3c44fde65c 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/jira_action_fields.tsx @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { JiraAction } from '../../../../../../common/types/action_types'; +import { JiraAction } from '../../../../../../../../common/types/action_types'; interface Props { action: JiraAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx index b70e504519ae59..7da2a22ecd6c45 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/logging_action_fields.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { LoggingAction } from '../../../../../../common/types/action_types'; +import { LoggingAction } from '../../../../../../../../common/types/action_types'; interface Props { action: LoggingAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx index b2b670bf6b91fb..3287bdefa08aa5 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/pagerduty_action_fields.tsx @@ -7,7 +7,7 @@ import React, { Fragment } from 'react'; import { EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { PagerDutyAction } from '../../../../../../common/types/action_types'; +import { PagerDutyAction } from '../../../../../../../../common/types/action_types'; interface Props { action: PagerDutyAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx index 7b5a598c97eb73..a72cf232d8d093 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/slack_action_fields.tsx @@ -6,7 +6,7 @@ import React, { Fragment } from 'react'; import { EuiComboBox, EuiTextArea, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SlackAction } from '../../../../../../common/types/action_types'; +import { SlackAction } from '../../../../../../../../common/types/action_types'; interface Props { action: SlackAction; diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx similarity index 98% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx index c3784e1ca55169..bdc6f0bcbb7172 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/action_fields/webhook_action_fields.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ErrableFormRow } from '../../../../../components/form_errors'; -import { WebhookAction } from '../../../../../../common/types/action_types'; +import { WebhookAction } from '../../../../../../../../common/types/action_types'; interface Props { action: WebhookAction; @@ -39,7 +39,7 @@ export const WebhookActionFields: React.FunctionComponent = ({ useEffect(() => { editAction({ key: 'contentType', value: 'application/json' }); // set content-type for threshold watch to json by default - }, []); + }, [editAction]); return ( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/index.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/index.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx similarity index 91% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx index 8b72eb7f194561..4fca772a182175 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_accordion.tsx @@ -21,13 +21,12 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ExecuteDetails } from 'plugins/watcher/models/execute_details/execute_details'; -import { Action } from 'plugins/watcher/models/action'; -import { toastNotifications } from 'ui/notify'; -import { WatchHistoryItem } from 'plugins/watcher/models/watch_history_item'; -import { ThresholdWatch } from 'plugins/watcher/models/watch/threshold_watch'; -import { ActionType } from '../../../../../common/types/action_types'; -import { ACTION_TYPES, ACTION_MODES } from '../../../../../common/constants'; +import { ExecuteDetails } from 'plugins/watcher/np_ready/application/models/execute_details/execute_details'; +import { Action } from 'plugins/watcher/np_ready/application/models/action'; +import { WatchHistoryItem } from 'plugins/watcher/np_ready/application/models/watch_history_item'; +import { ThresholdWatch } from 'plugins/watcher/np_ready/application/models/watch/threshold_watch'; +import { ActionType } from '../../../../../../../common/types/action_types'; +import { ACTION_TYPES, ACTION_MODES } from '../../../../../../../common/constants'; import { WatchContext } from '../../watch_context'; import { WebhookActionFields, @@ -39,8 +38,8 @@ import { JiraActionFields, } from './action_fields'; import { executeWatch } from '../../../../lib/api'; -import { watchActionsConfigurationMap } from '../../../../lib/documentation_links'; import { SectionError } from '../../../../components'; +import { useAppContext } from '../../../../app_context'; const actionFieldsComponentMap = { [ACTION_TYPES.LOGGING]: LoggingActionFields, @@ -71,6 +70,10 @@ export const WatchActionsAccordion: React.FunctionComponent = ({ settings, actionErrors, }) => { + const { + links: { watchActionsConfigurationMap }, + toasts, + } = useAppContext(); const { watch, setWatchProperty } = useContext(WatchContext); const { actions } = watch; @@ -238,9 +241,9 @@ export const WatchActionsAccordion: React.FunctionComponent = ({ if (actionStatus && actionStatus.lastExecutionSuccessful === false) { const message = actionStatus.lastExecutionReason || action.simulateFailMessage; - return toastNotifications.addDanger(message); + return toasts.addDanger(message); } - return toastNotifications.addSuccess(action.simulateMessage); + return toasts.addSuccess(action.simulateMessage); }} > {action.simulatePrompt} diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx index 82f3352b4e0235..d92cccfa00f148 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_dropdown.tsx @@ -16,9 +16,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext, useState } from 'react'; -import { Action } from 'plugins/watcher/models/action'; +import { Action } from 'plugins/watcher/np_ready/application/models/action'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ACTION_TYPES } from '../../../../../common/constants'; +import { ACTION_TYPES } from '../../../../../../../common/constants'; import { WatchContext } from '../../watch_context'; const disabledMessage = i18n.translate( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx similarity index 93% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx index a2e46652429ea0..6072f93e53cf64 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_action_panel.tsx @@ -6,7 +6,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { loadSettings } from '../../../../lib/api'; +import { useLoadSettings } from '../../../../lib/api'; import { WatchActionsDropdown } from './threshold_watch_action_dropdown'; import { WatchActionsAccordion } from './threshold_watch_action_accordion'; import { WatchContext } from '../../watch_context'; @@ -22,7 +22,7 @@ interface Props { export const WatchActionsPanel: React.FunctionComponent = ({ actionErrors }) => { const { watch } = useContext(WatchContext); - const { data: settings, isLoading } = loadSettings(); + const { data: settings, isLoading } = useLoadSettings(); return (
diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx similarity index 95% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx index 910d4f1e0b15c2..f1b5d2c9eab7b8 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/threshold_watch_edit.tsx @@ -26,9 +26,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TIME_UNITS } from '../../../../../common/constants'; -import { serializeThresholdWatch } from '../../../../../common/lib/serialization'; -import { ErrableFormRow, SectionError } from '../../../../components'; +import { TIME_UNITS } from '../../../../../../../common/constants'; +import { serializeThresholdWatch } from '../../../../../../../common/lib/serialization'; +import { ErrableFormRow, SectionError, Error as ServerError } from '../../../../components'; import { fetchFields, getMatchingIndices, loadIndexPatterns } from '../../../../lib/api'; import { aggTypes } from '../../../../models/watch/agg_types'; import { groupByTypes } from '../../../../models/watch/group_by_types'; @@ -40,6 +40,7 @@ import { WatchActionsPanel } from './threshold_watch_action_panel'; import { getTimeUnitLabel } from '../../../../lib/get_time_unit_label'; import { goToWatchList } from '../../../../lib/navigation'; import { RequestFlyout } from '../request_flyout'; +import { useAppContext } from '../../../../app_context'; const expressionFieldsWithValidation = [ 'aggField', @@ -104,7 +105,7 @@ const getTimeFieldOptions = (fields: any) => { }; interface IOption { label: string; - options: Array<{ value: string; label: string }>; + options: Array<{ value: string; label: string; key?: string }>; } const getIndexOptions = async (patternString: string, indexPatterns: string[]) => { @@ -129,12 +130,14 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = defaultMessage: 'Based on your indices and index patterns', } ), - options: matchingOptions.map(match => { - return { - label: match, - value: match, - }; - }), + options: matchingOptions + .map(match => { + return { + label: match, + value: match, + }; + }) + .sort((a, b) => String(a.label).localeCompare(b.label)), }); } @@ -144,6 +147,7 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = }), options: [ { + key: 'UNIQUE_CHOOSE_KEY', value: patternString, label: patternString, }, @@ -155,7 +159,8 @@ const getIndexOptions = async (patternString: string, indexPatterns: string[]) = export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { // hooks - const [indexPatterns, setIndexPatterns] = useState([]); + const { toasts } = useAppContext(); + const [indexPatterns, setIndexPatterns] = useState([]); const [esFields, setEsFields] = useState([]); const [indexOptions, setIndexOptions] = useState([]); const [timeFieldOptions, setTimeFieldOptions] = useState([firstFieldOption]); @@ -165,34 +170,33 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { const [watchThresholdPopoverOpen, setWatchThresholdPopoverOpen] = useState(false); const [watchDurationPopoverOpen, setWatchDurationPopoverOpen] = useState(false); const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false); - const [serverError, setServerError] = useState<{ - data: { nessage: string; error: string }; - } | null>(null); + const [serverError, setServerError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [isIndiciesLoading, setIsIndiciesLoading] = useState(false); const [isRequestVisible, setIsRequestVisible] = useState(false); const { watch, setWatchProperty } = useContext(WatchContext); - const getIndexPatterns = async () => { - const indexPatternObjects = await loadIndexPatterns(); - const titles = indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); - setIndexPatterns(titles); - }; + useEffect(() => { + const getIndexPatterns = async () => { + const indexPatternObjects = await loadIndexPatterns(); + const titles = indexPatternObjects.map((indexPattern: any) => indexPattern.attributes.title); + setIndexPatterns(titles); + }; - const loadData = async () => { - if (watch.index && watch.index.length > 0) { - const allEsFields = await getFields(watch.index); - const timeFields = getTimeFieldOptions(allEsFields); - setEsFields(allEsFields); - setTimeFieldOptions(timeFields); - setWatchProperty('timeFields', timeFields); - } - getIndexPatterns(); - }; + const loadData = async () => { + if (watch.index && watch.index.length > 0) { + const allEsFields = await getFields(watch.index); + const timeFields = getTimeFieldOptions(allEsFields); + setEsFields(allEsFields); + setTimeFieldOptions(timeFields); + setWatchProperty('timeFields', timeFields); + } + getIndexPatterns(); + }; - useEffect(() => { loadData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const { errors } = watch.validate(); @@ -899,7 +903,7 @@ export const ThresholdWatchEdit = ({ pageTitle }: { pageTitle: string }) => { isLoading={isSaving} onClick={async () => { setIsSaving(true); - const savedWatch = await onWatchSave(watch); + const savedWatch = await onWatchSave(watch, toasts); if (savedWatch && savedWatch.error) { setIsSaving(false); return setServerError(savedWatch.error); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx similarity index 83% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index 772f3cc024fe86..a3da7d14c88869 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -18,20 +18,20 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import { TimeBuckets } from 'ui/time_buckets'; import dateMath from '@elastic/datemath'; -import chrome from 'ui/chrome'; import moment from 'moment-timezone'; +import { IUiSettingsClient } from 'src/core/public'; import { EuiCallOut, EuiLoadingChart, EuiSpacer, EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { VisualizeOptions } from 'plugins/watcher/models/visualize_options'; -import { ThresholdWatch } from 'plugins/watcher/models/watch/threshold_watch'; -import { npStart } from 'ui/new_platform'; -import { getWatchVisualizationData } from '../../../../lib/api'; +import { VisualizeOptions } from 'plugins/watcher/np_ready/application/models/visualize_options'; +import { ThresholdWatch } from 'plugins/watcher/np_ready/application/models/watch/threshold_watch'; + +import { useGetWatchVisualizationData } from '../../../../lib/api'; import { WatchContext } from '../../watch_context'; import { aggTypes } from '../../../../models/watch/agg_types'; import { comparators } from '../../../../models/watch/comparators'; import { SectionError, Error } from '../../../../components'; +import { useAppContext } from '../../../../app_context'; const customTheme = () => { return { @@ -46,8 +46,7 @@ const customTheme = () => { }; }; -const getTimezone = () => { - const config = chrome.getUiSettingsClient(); +const getTimezone = (config: IUiSettingsClient) => { const DATE_FORMAT_CONFIG_KEY = 'dateFormat:tz'; const isCustomTimezone = !config.isDefault(DATE_FORMAT_CONFIG_KEY); if (isCustomTimezone) { @@ -59,8 +58,7 @@ const getTimezone = () => { return detectedTimezone; } // default to UTC if we can't figure out the timezone - const tzOffset = moment().format('Z'); - return tzOffset; + return moment().format('Z'); }; const getDomain = (watch: any) => { @@ -83,16 +81,20 @@ const getThreshold = (watch: any) => { return watch.threshold.slice(0, comparators[watch.thresholdComparator].requiredValues); }; -const getTimeBuckets = (watch: any) => { +const getTimeBuckets = (watch: any, timeBuckets: any) => { const domain = getDomain(watch); - const timeBuckets = new TimeBuckets(); timeBuckets.setBounds(domain); return timeBuckets; }; export const WatchVisualization = () => { + const { + legacy: { TimeBuckets }, + euiUtils, + uiSettings, + } = useAppContext(); const { watch } = useContext(WatchContext); - const chartsTheme = npStart.plugins.eui_utils.useChartsTheme(); + const chartsTheme = euiUtils.useChartsTheme(); const { index, timeField, @@ -117,7 +119,7 @@ export const WatchVisualization = () => { rangeFrom: domain.min, rangeTo: domain.max, interval, - timezone: getTimezone(), + timezone: getTimezone(uiSettings), }); // Fetching visualization data is independent of watch actions @@ -129,30 +131,33 @@ export const WatchVisualization = () => { data: watchVisualizationData, error, sendRequest: reload, - } = getWatchVisualizationData(watchWithoutActions, visualizeOptions); + } = useGetWatchVisualizationData(watchWithoutActions, visualizeOptions); - useEffect(() => { - // Prevent sending a second request on initial render. - if (isInitialRequest) { - return; - } - - reload(); - }, [ - index, - timeField, - triggerIntervalSize, - triggerIntervalUnit, - aggType, - aggField, - termSize, - termField, - thresholdComparator, - timeWindowSize, - timeWindowUnit, - groupBy, - threshold, - ]); + useEffect( + () => { + // Prevent sending a second request on initial render. + if (isInitialRequest) { + return; + } + reload(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + index, + timeField, + triggerIntervalSize, + triggerIntervalUnit, + aggType, + aggField, + termSize, + termField, + thresholdComparator, + timeWindowSize, + timeWindowUnit, + groupBy, + threshold, + ] + ); if (isInitialRequest && isLoading) { return ( @@ -190,7 +195,7 @@ export const WatchVisualization = () => { if (watchVisualizationData) { const watchVisualizationDataKeys = Object.keys(watchVisualizationData); - const timezone = getTimezone(); + const timezone = getTimezone(uiSettings); const actualThreshold = getThreshold(watch); let maxY = actualThreshold[actualThreshold.length - 1]; @@ -204,7 +209,7 @@ export const WatchVisualization = () => { const dateFormatter = (d: number) => { return moment(d) .tz(timezone) - .format(getTimeBuckets(watch).getScaledDateFormat()); + .format(getTimeBuckets(watch, new TimeBuckets()).getScaledDateFormat()); }; const aggLabel = aggTypes[watch.aggType].text; return ( diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx similarity index 82% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx index 25daf190dc1b12..9f252d3e542e0e 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/components/watch_edit.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/components/watch_edit.tsx @@ -9,13 +9,11 @@ import { isEqual } from 'lodash'; import { EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { Watch } from 'plugins/watcher/models/watch'; +import { Watch } from 'plugins/watcher/np_ready/application/models/watch'; import { FormattedMessage } from '@kbn/i18n/react'; -import { WATCH_TYPES } from '../../../../common/constants'; -import { BaseWatch } from '../../../../common/types/watch_types'; +import { WATCH_TYPES } from '../../../../../../common/constants'; +import { BaseWatch } from '../../../../../../common/types/watch_types'; import { getPageErrorCode, PageError, SectionLoading, SectionError } from '../../../components'; import { loadWatch } from '../../../lib/api'; import { listBreadcrumb, editBreadcrumb, createBreadcrumb } from '../../../lib/breadcrumbs'; @@ -23,6 +21,7 @@ import { JsonWatchEdit } from './json_watch_edit'; import { ThresholdWatchEdit } from './threshold_watch_edit'; import { MonitoringWatchEdit } from './monitoring_watch_edit'; import { WatchContext } from '../watch_context'; +import { useAppContext } from '../../../app_context'; const getTitle = (watch: BaseWatch) => { if (watch.isNew) { @@ -97,6 +96,10 @@ export const WatchEdit = ({ }; }) => { // hooks + const { + legacy: { MANAGEMENT_BREADCRUMB }, + chrome, + } = useAppContext(); const [{ watch, loadError }, dispatch] = useReducer(watchReducer, { watch: null }); const setWatchProperty = (property: string, value: any) => { @@ -107,33 +110,33 @@ export const WatchEdit = ({ dispatch({ command: 'addAction', payload: action }); }; - const getWatch = async () => { - if (id) { - try { - const loadedWatch = await loadWatch(id); - dispatch({ command: 'setWatch', payload: loadedWatch }); - } catch (error) { - dispatch({ command: 'setError', payload: error }); - } - } else if (type) { - const WatchType = Watch.getWatchTypes()[type]; - if (WatchType) { - dispatch({ command: 'setWatch', payload: new WatchType() }); + useEffect(() => { + const getWatch = async () => { + if (id) { + try { + const loadedWatch = await loadWatch(id); + dispatch({ command: 'setWatch', payload: loadedWatch }); + } catch (error) { + dispatch({ command: 'setError', payload: error }); + } + } else if (type) { + const WatchType = Watch.getWatchTypes()[type]; + if (WatchType) { + dispatch({ command: 'setWatch', payload: new WatchType() }); + } } - } - }; + }; - useEffect(() => { getWatch(); - }, []); + }, [id, type]); useEffect(() => { - chrome.breadcrumbs.set([ + chrome.setBreadcrumbs([ MANAGEMENT_BREADCRUMB, listBreadcrumb, id ? editBreadcrumb : createBreadcrumb, ]); - }, [id]); + }, [id, chrome, MANAGEMENT_BREADCRUMB]); const errorCode = getPageErrorCode(loadError); if (errorCode) { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_context.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_context.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_context.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_context.ts diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts similarity index 86% rename from x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts index 320ba59e0589ee..b93c2c510047db 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_edit/watch_edit_actions.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_edit/watch_edit_actions.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ToastsSetup } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; import { get } from 'lodash'; -import { ACTION_TYPES, WATCH_TYPES } from '../../../common/constants'; -import { BaseWatch } from '../../../common/types/watch_types'; +import { ACTION_TYPES, WATCH_TYPES } from '../../../../../common/constants'; +import { BaseWatch } from '../../../../../common/types/watch_types'; import { createWatch } from '../../lib/api'; import { goToWatchList } from '../../lib/navigation'; @@ -62,10 +62,10 @@ function createActionsForWatch(watchInstance: BaseWatch) { return watchInstance; } -export async function saveWatch(watch: BaseWatch): Promise { +export async function saveWatch(watch: BaseWatch, toasts: ToastsSetup): Promise { try { await createWatch(watch); - toastNotifications.addSuccess( + toasts.addSuccess( i18n.translate('xpack.watcher.sections.watchEdit.json.saveSuccessNotificationText', { defaultMessage: "Saved '{watchDisplayName}'", values: { @@ -75,11 +75,11 @@ export async function saveWatch(watch: BaseWatch): Promise { ); goToWatchList(); } catch (error) { - return error.response ? { error: error.response } : { error }; + return { error: error?.response.data ?? (error.body || error) }; } } -export async function onWatchSave(watch: BaseWatch): Promise { +export async function onWatchSave(watch: BaseWatch, toasts: ToastsSetup): Promise { const watchActions = watch.watch && watch.watch.actions; const watchData = watchActions ? createActionsForWatch(watch) : watch; @@ -109,7 +109,7 @@ export async function onWatchSave(watch: BaseWatch): Promise { }, }; } - return saveWatch(watchData); + return saveWatch(watchData, toasts); } - return saveWatch(watchData); + return saveWatch(watchData, toasts); } diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx index d5191c56643c24..b2afc0b92509bf 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_list/components/watch_list.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_list/components/watch_list.tsx @@ -27,10 +27,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Moment } from 'moment'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { REFRESH_INTERVALS, PAGINATION, WATCH_TYPES } from '../../../../common/constants'; +import { REFRESH_INTERVALS, PAGINATION, WATCH_TYPES } from '../../../../../../common/constants'; import { listBreadcrumb } from '../../../lib/breadcrumbs'; import { getPageErrorCode, @@ -41,12 +39,17 @@ import { SectionLoading, Error, } from '../../../components'; -import { loadWatches } from '../../../lib/api'; -import { watcherGettingStartedUrl } from '../../../lib/documentation_links'; +import { useLoadWatches } from '../../../lib/api'; import { goToCreateThresholdAlert, goToCreateAdvancedWatch } from '../../../lib/navigation'; +import { useAppContext } from '../../../app_context'; export const WatchList = () => { // hooks + const { + chrome, + legacy: { MANAGEMENT_BREADCRUMB }, + links: { watcherGettingStartedUrl }, + } = useAppContext(); const [selection, setSelection] = useState([]); const [watchesToDelete, setWatchesToDelete] = useState([]); // Filter out deleted watches on the client, because the API will return 200 even though some watches @@ -54,10 +57,10 @@ export const WatchList = () => { const [deletedWatches, setDeletedWatches] = useState([]); useEffect(() => { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb]); - }, []); + chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb]); + }, [chrome, MANAGEMENT_BREADCRUMB]); - const { isLoading: isWatchesLoading, data: watches, error } = loadWatches( + const { isLoading: isWatchesLoading, data: watches, error } = useLoadWatches( REFRESH_INTERVALS.WATCH_LIST ); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx similarity index 96% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx index aba4fd0c52a2e1..197342bba4180c 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_detail.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_detail.tsx @@ -7,7 +7,6 @@ import React, { Fragment, useState, useEffect, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { EuiInMemoryTable, @@ -21,8 +20,9 @@ import { } from '@elastic/eui'; import { ackWatchAction } from '../../../lib/api'; import { WatchStatus } from '../../../components'; -import { PAGINATION } from '../../../../common/constants'; +import { PAGINATION } from '../../../../../../common/constants'; import { WatchDetailsContext } from '../watch_details_context'; +import { useAppContext } from '../../../app_context'; interface ActionError { code: string; @@ -36,6 +36,7 @@ interface ActionStatus { } export const WatchDetail = () => { + const { toasts } = useAppContext(); const { watchDetail } = useContext(WatchDetailsContext); const [actionStatuses, setActionStatuses] = useState([]); @@ -60,7 +61,7 @@ export const WatchDetail = () => { }; }); setActionStatuses(actionStatusesWithErrors); - }, [watchDetail]); + }, [watchDetail, actionErrors, currentActionStatuses]); const baseColumns = [ { @@ -144,7 +145,7 @@ export const WatchDetail = () => { return setActionStatuses(newActionStatusesWithErrors); } catch (e) { setIsActionStatusLoading(false); - toastNotifications.addDanger( + toasts.addDanger( i18n.translate( 'xpack.watcher.sections.watchDetail.watchTable.ackActionErrorMessage', { diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx similarity index 97% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx index bf6ca0c6c43a05..2bc1a0cbace186 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_history.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_history.tsx @@ -23,9 +23,9 @@ import { EuiTitle, } from '@elastic/eui'; -import { PAGINATION } from '../../../../common/constants'; +import { PAGINATION } from '../../../../../../common/constants'; import { WatchStatus, SectionError, Error } from '../../../components'; -import { loadWatchHistory, loadWatchHistoryDetail } from '../../../lib/api'; +import { useLoadWatchHistory, useLoadWatchHistoryDetail } from '../../../lib/api'; import { WatchDetailsContext } from '../watch_details_context'; const watchHistoryTimeSpanOptions = [ @@ -83,12 +83,12 @@ export const WatchHistory = () => { setIsActivated(isActive); } - const { error: historyError, data: history, isLoading } = loadWatchHistory( + const { error: historyError, data: history, isLoading } = useLoadWatchHistory( loadedWatch.id, watchHistoryTimeSpan ); - const { error: watchHistoryDetailsError, data: watchHistoryDetails } = loadWatchHistoryDetail( + const { error: watchHistoryDetailsError, data: watchHistoryDetails } = useLoadWatchHistoryDetail( detailWatchId ); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx similarity index 94% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx index 413e8f638887ba..53817c23e72eb7 100644 --- a/x-pack/legacy/plugins/watcher/public/sections/watch_status/components/watch_status.tsx +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/components/watch_status.tsx @@ -17,15 +17,12 @@ import { EuiBadge, EuiButtonEmpty, } from '@elastic/eui'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; import { WatchDetail } from './watch_detail'; import { WatchHistory } from './watch_history'; import { listBreadcrumb, statusBreadcrumb } from '../../../lib/breadcrumbs'; -import { loadWatchDetail, deactivateWatch, activateWatch } from '../../../lib/api'; +import { useLoadWatchDetail, deactivateWatch, activateWatch } from '../../../lib/api'; import { WatchDetailsContext } from '../watch_details_context'; import { getPageErrorCode, @@ -34,6 +31,7 @@ import { DeleteWatchesModal, } from '../../../components'; import { goToWatchList } from '../../../lib/navigation'; +import { useAppContext } from '../../../app_context'; interface WatchStatusTab { id: string; @@ -69,11 +67,16 @@ export const WatchStatus = ({ }; }; }) => { + const { + chrome, + legacy: { MANAGEMENT_BREADCRUMB }, + toasts, + } = useAppContext(); const { error: watchDetailError, data: watchDetail, isLoading: isWatchDetailLoading, - } = loadWatchDetail(id); + } = useLoadWatchDetail(id); const [selectedTab, setSelectedTab] = useState(WATCH_EXECUTION_HISTORY_TAB); const [isActivated, setIsActivated] = useState(undefined); @@ -81,8 +84,8 @@ export const WatchStatus = ({ const [isTogglingActivation, setIsTogglingActivation] = useState(false); useEffect(() => { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]); - }, [id]); + chrome.setBreadcrumbs([MANAGEMENT_BREADCRUMB, listBreadcrumb, statusBreadcrumb]); + }, [id, chrome, MANAGEMENT_BREADCRUMB]); const errorCode = getPageErrorCode(watchDetailError); @@ -148,7 +151,7 @@ export const WatchStatus = ({ defaultMessage: "Couldn't activate watch", } ); - return toastNotifications.addDanger(message); + return toasts.addDanger(message); } setIsActivated(!isActivated); diff --git a/x-pack/legacy/plugins/watcher/public/sections/watch_status/watch_details_context.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/watch_details_context.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/public/sections/watch_status/watch_details_context.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/sections/watch_status/watch_details_context.ts diff --git a/x-pack/legacy/plugins/watcher/public/shared_imports.ts b/x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts similarity index 79% rename from x-pack/legacy/plugins/watcher/public/shared_imports.ts rename to x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts index 3d93b882733abf..60445b00c09858 100644 --- a/x-pack/legacy/plugins/watcher/public/shared_imports.ts +++ b/x-pack/legacy/plugins/watcher/public/np_ready/application/shared_imports.ts @@ -10,4 +10,4 @@ export { UseRequestConfig, sendRequest, useRequest, -} from '../../../../../src/plugins/es_ui_shared/public/request'; +} from '../../../../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/index.ts b/x-pack/legacy/plugins/watcher/public/np_ready/index.ts new file mode 100644 index 00000000000000..ff635579316e58 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { WatcherUIPlugin } from './plugin'; + +export const plugin = () => new WatcherUIPlugin(); diff --git a/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts b/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts new file mode 100644 index 00000000000000..161de9b5fc0606 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/np_ready/plugin.ts @@ -0,0 +1,62 @@ +/* + * 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 { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { LegacyDependencies } from './types'; + +interface LegacyPlugins { + __LEGACY: LegacyDependencies; +} + +export class WatcherUIPlugin implements Plugin { + /* TODO: Remove this in future. We need this at mount (setup) but it's only available on start plugins. */ + euiUtils: any = null; + + setup({ application, notifications, http, uiSettings }: CoreSetup, { __LEGACY }: LegacyPlugins) { + application.register({ + id: 'watcher', + title: 'Watcher', + mount: async ( + { + core: { + docLinks, + chrome, + // Waiting for types to be updated. + // @ts-ignore + savedObjects, + i18n: { Context: I18nContext }, + }, + }, + { element } + ) => { + const euiUtils = this.euiUtils!; + const { boot } = await import('./application/boot'); + return boot({ + element, + toasts: notifications.toasts, + http, + uiSettings, + docLinks, + chrome, + euiUtils, + savedObjects: savedObjects.client, + I18nContext, + legacy: { + ...__LEGACY, + }, + }); + }, + }); + } + + start(core: CoreStart, { eui_utils }: any) { + // eslint-disable-next-line @typescript-eslint/camelcase + this.euiUtils = eui_utils; + } + + stop() {} +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js b/x-pack/legacy/plugins/watcher/public/np_ready/types.ts similarity index 63% rename from x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js rename to x-pack/legacy/plugins/watcher/public/np_ready/types.ts index eefb320e9b1d93..22109f99c2c48b 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_settings_routes.js +++ b/x-pack/legacy/plugins/watcher/public/np_ready/types.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { registerLoadRoute } from './register_load_route'; - -export function registerSettingsRoutes(server) { - registerLoadRoute(server); +export interface LegacyDependencies { + MANAGEMENT_BREADCRUMB: { text: string; href?: string }; + TimeBuckets: any; + licenseStatus: any; } diff --git a/x-pack/legacy/plugins/watcher/public/register_feature.js b/x-pack/legacy/plugins/watcher/public/register_feature.js deleted file mode 100644 index 5dd4f28f03bc50..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_feature.js +++ /dev/null @@ -1,24 +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 { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; -import { i18n } from '@kbn/i18n'; - -FeatureCatalogueRegistryProvider.register(() => { - return { - id: 'watcher', - title: 'Watcher', // This is a product name so we don't translate it. - description: i18n.translate('xpack.watcher.watcherDescription', { - defaultMessage: 'Detect changes in your data by creating, managing, and monitoring alerts.' - }), - icon: 'watchesApp', - path: '/app/kibana#/management/elasticsearch/watcher/watches', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN - }; -}); diff --git a/x-pack/legacy/plugins/watcher/public/register_feature.ts b/x-pack/legacy/plugins/watcher/public/register_feature.ts new file mode 100644 index 00000000000000..0de41e09f788e1 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/public/register_feature.ts @@ -0,0 +1,21 @@ +/* + * 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 { npSetup } from 'ui/new_platform'; +import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; + +npSetup.plugins.home.featureCatalogue.register({ + id: 'watcher', + title: 'Watcher', // This is a product name so we don't translate it. + category: FeatureCatalogueCategory.ADMIN, + description: i18n.translate('xpack.watcher.watcherDescription', { + defaultMessage: 'Detect changes in your data by creating, managing, and monitoring alerts.', + }), + icon: 'watchesApp', + path: '/app/kibana#/management/elasticsearch/watcher/watches', + showOnHomePage: true, +}); diff --git a/x-pack/legacy/plugins/watcher/public/register_management_sections.js b/x-pack/legacy/plugins/watcher/public/register_management_sections.js deleted file mode 100644 index 886ac7d28db64d..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_management_sections.js +++ /dev/null @@ -1,60 +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 { management } from 'ui/management'; -import { i18n } from '@kbn/i18n'; - -management.getSection('elasticsearch').register('watcher', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watcherDisplayName', { - defaultMessage: 'Watcher', - }), - order: 6, - url: '#/management/elasticsearch/watcher/', -}); - -management.getSection('elasticsearch/watcher').register('watches', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.watchesDisplayName', { - defaultMessage: 'Watches', - }), - order: 1, -}); - -management.getSection('elasticsearch/watcher').register('watch', { - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('status', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.statusDisplayName', { - defaultMessage: 'Status', - }), - order: 1, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('edit', { - display: i18n.translate('xpack.watcher.sections.watchList.managementSection.editDisplayName', { - defaultMessage: 'Edit', - }), - order: 2, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('new', { - display: i18n.translate( - 'xpack.watcher.sections.watchList.managementSection.newWatchDisplayName', - { - defaultMessage: 'New Watch', - } - ), - order: 1, - visible: false, -}); - -management.getSection('elasticsearch/watcher/watch').register('history-item', { - order: 1, - visible: false, -}); diff --git a/x-pack/legacy/plugins/watcher/public/register_route.js b/x-pack/legacy/plugins/watcher/public/register_route.js deleted file mode 100644 index c58be17bc6e758..00000000000000 --- a/x-pack/legacy/plugins/watcher/public/register_route.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SavedObjectsClientProvider } from 'ui/saved_objects'; -import routes from 'ui/routes'; -import { management } from 'ui/management'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import template from './app.html'; -import { App } from './app'; -import { setHttpClient, setSavedObjectsClient } from './lib/api'; -import { I18nContext } from 'ui/i18n'; -import { manageAngularLifecycle } from './lib/manage_angular_lifecycle'; -import { PLUGIN } from '../common/constants'; -import { LICENSE_STATUS_UNAVAILABLE, LICENSE_STATUS_INVALID } from '../../../common/constants'; - -let elem; -const renderReact = async (elem, licenseStatus) => { - render( - - - , - elem - ); -}; -routes.when('/management/elasticsearch/watcher/:param1?/:param2?/:param3?/:param4?', { - template, - controller: class WatcherController { - constructor($injector, $scope, $http, Private) { - const $route = $injector.get('$route'); - const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); - - // clean up previously rendered React app if one exists - // this happens because of React Router redirects - elem && unmountComponentAtNode(elem); - setSavedObjectsClient(Private(SavedObjectsClientProvider)); - // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, - // e.g. to check license status per request. - setHttpClient($http); - $scope.$$postDigest(() => { - elem = document.getElementById('watchReactRoot'); - renderReact(elem, licenseStatus); - manageAngularLifecycle($scope, $route, elem); - }); - } - }, - controllerAs: 'watchRoute', -}); - -routes.defaults(/\/management/, { - resolve: { - watcherManagementSection: () => { - const watchesSection = management.getSection('elasticsearch/watcher'); - const licenseStatus = xpackInfo.get(`features.${PLUGIN.ID}`); - const { status } = licenseStatus; - - if (status === LICENSE_STATUS_INVALID || status === LICENSE_STATUS_UNAVAILABLE) { - return watchesSection.hide(); - } - - watchesSection.show(); - - }, - }, -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js deleted file mode 100644 index b0ca0906010623..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/call_with_internal_user_factory.js +++ /dev/null @@ -1,18 +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 { once } from 'lodash'; - -const _callWithInternalUser = once((server) => { - const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin'); - return callWithInternalUser; -}); - -export const callWithInternalUserFactory = (server) => { - return (...args) => { - return _callWithInternalUser(server)(...args); - }; -}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js deleted file mode 100644 index a56a50e2864a54..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_internal_user_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callWithInternalUserFactory } from './call_with_internal_user_factory'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index f60f825b980043..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../elasticsearch_js_plugin'; - -const callWithRequest = once((server) => { - const config = { plugins: [ elasticsearchJsPlugin ] }; - const cluster = server.plugins.elasticsearch.createCluster('watcher', config); - - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff94..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js b/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js deleted file mode 100644 index 87b5ff5426c9de..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff3..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index 467cc4fcdae1f1..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad4..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +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 expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js deleted file mode 100644 index f275f156370912..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5a..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +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 Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js deleted file mode 100644 index 2df2e4b802e1a1..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,30 +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 Boom from 'boom'; - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { - - const statusCode = err.statusCode; - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response and return it - if (!statusCodeToMessageMap[statusCode]) { - return Boom.boomify(err, { statusCode }); - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js deleted file mode 100644 index ffd915c5133626..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/error_wrappers/wrap_unknown_error.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 76fdf7b36c3d0f..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,48 +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 expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js deleted file mode 100644 index 441648a8701e08..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js deleted file mode 100644 index 80daac5bd496dc..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/is_es_error_factory/is_es_error_factory.js +++ /dev/null @@ -1,18 +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 { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server) { - const esErrors = esErrorsFactory(server); - return function isEsError(err) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 5b34108c9c1c01..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/license_pre_routing_factory.js +++ /dev/null @@ -1,31 +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 { once } from 'lodash'; -import { wrapCustomError } from '../error_wrappers'; -import { PLUGIN } from '../../../common/constants'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; - -export const licensePreRoutingFactory = once((server) => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - const { status } = licenseCheckResults; - - if (status !== LICENSE_STATUS_VALID) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - - return null; - } - - return licensePreRouting; -}); - diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts b/x-pack/legacy/plugins/watcher/server/np_ready/index.ts similarity index 55% rename from x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts rename to x-pack/legacy/plugins/watcher/server/np_ready/index.ts index 7363c5a27f64b9..3f5e1a91209ead 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/index.ts +++ b/x-pack/legacy/plugins/watcher/server/np_ready/index.ts @@ -3,8 +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 { PluginInitializerContext } from 'src/core/server'; +import { WatcherServerPlugin } from './plugin'; -import './pages/analytics_exploration/directive'; -import './pages/analytics_exploration/route'; -import './pages/analytics_management/directive'; -import './pages/analytics_management/route'; +export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin(); diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.ts new file mode 100644 index 00000000000000..eaec9cd91b23cd --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/call_with_request_factory.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 { ElasticsearchServiceSetup } from 'src/core/server'; +import { once } from 'lodash'; +import { elasticsearchJsPlugin } from './elasticsearch_js_plugin'; + +const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { + const config = { plugins: [elasticsearchJsPlugin] }; + return elasticsearchService.createClient('watcher', config); +}); + +export const callWithRequestFactory = ( + elasticsearchService: ElasticsearchServiceSetup, + request: any +) => { + return (...args: any[]) => { + return ( + callWithRequest(elasticsearchService) + .asScoped(request) + // @ts-ignore + .callAsCurrentUser(...args) + ); + }; +}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts similarity index 84% rename from x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts index ad42388beea1eb..240e93e160fe0c 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/elasticsearch_js_plugin/elasticsearch_js_plugin.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/elasticsearch_js_plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const elasticsearchJsPlugin = (Client, config, components) => { +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { const ca = components.clientAction.factory; Client.prototype.watcher = components.clientAction.namespaceFactory(); @@ -21,19 +21,19 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_deactivate', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'PUT' + method: 'PUT', }); /** @@ -47,19 +47,19 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_activate', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'PUT' + method: 'PUT', }); /** @@ -74,23 +74,23 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>/_ack/<%=action%>', req: { id: { type: 'string', - required: true + required: true, }, action: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'POST' + method: 'POST', }); /** @@ -105,22 +105,22 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' + type: 'duration', }, force: { - type: 'boolean' - } + type: 'boolean', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, - method: 'DELETE' + method: 'DELETE', }); /** @@ -132,14 +132,14 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { - fmt: '/_watcher/watch/_execute' + fmt: '/_watcher/watch/_execute', }, needBody: true, - method: 'POST' + method: 'POST', }); /** @@ -155,10 +155,10 @@ export const elasticsearchJsPlugin = (Client, config, components) => { req: { id: { type: 'string', - required: true - } - } - } + required: true, + }, + }, + }, }); /** @@ -172,20 +172,20 @@ export const elasticsearchJsPlugin = (Client, config, components) => { params: { masterTimeout: { name: 'master_timeout', - type: 'duration' - } + type: 'duration', + }, }, url: { fmt: '/_watcher/watch/<%=id%>', req: { id: { type: 'string', - required: true - } - } + required: true, + }, + }, }, needBody: true, - method: 'PUT' + method: 'PUT', }); /** @@ -196,9 +196,9 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.restart = ca({ params: {}, url: { - fmt: '/_watcher/_restart' + fmt: '/_watcher/_restart', }, - method: 'PUT' + method: 'PUT', }); /** @@ -209,9 +209,9 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.start = ca({ params: {}, url: { - fmt: '/_watcher/_start' + fmt: '/_watcher/_start', }, - method: 'PUT' + method: 'PUT', }); /** @@ -222,8 +222,8 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.stats = ca({ params: {}, url: { - fmt: '/_watcher/stats' - } + fmt: '/_watcher/stats', + }, }); /** @@ -234,8 +234,8 @@ export const elasticsearchJsPlugin = (Client, config, components) => { watcher.stop = ca({ params: {}, url: { - fmt: '/_watcher/_stop' + fmt: '/_watcher/_stop', }, - method: 'PUT' + method: 'PUT', }); }; diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts similarity index 64% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts index eb76d5d3731cf9..d762b05f01d79e 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/fetch_all_from_scroll.ts @@ -5,9 +5,9 @@ */ import { get } from 'lodash'; -import { ES_SCROLL_SETTINGS } from '../../../common/constants'; +import { ES_SCROLL_SETTINGS } from '../../../../common/constants'; -export function fetchAllFromScroll(response, callWithRequest, hits = []) { +export function fetchAllFromScroll(response: any, callWithRequest: any, hits: any[] = []) { const newHits = get(response, 'hits.hits', []); const scrollId = get(response, '_scroll_id'); @@ -17,12 +17,11 @@ export function fetchAllFromScroll(response, callWithRequest, hits = []) { return callWithRequest('scroll', { body: { scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - scroll_id: scrollId - } - }) - .then(innerResponse => { - return fetchAllFromScroll(innerResponse, callWithRequest, hits); - }); + scroll_id: scrollId, + }, + }).then((innerResponse: any) => { + return fetchAllFromScroll(innerResponse, callWithRequest, hits); + }); } return Promise.resolve(hits); diff --git a/x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/fetch_all_from_scroll/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/fetch_all_from_scroll/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/index.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/index.ts new file mode 100644 index 00000000000000..a9a3c61472d8c7 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/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 { isEsError } from './is_es_error'; diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.ts new file mode 100644 index 00000000000000..4137293cf39c06 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/is_es_error/is_es_error.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 * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js similarity index 71% rename from x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js index ed4a51a11b7cda..fc01e42e6fdf2f 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; +import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; import { licensePreRoutingFactory } from '../license_pre_routing_factory'; -import { LICENSE_STATUS_VALID, LICENSE_STATUS_EXPIRED } from '../../../../../../common/constants/license_status'; +import { LICENSE_STATUS_VALID, LICENSE_STATUS_EXPIRED } from '../../../../../../../common/constants/license_status'; describe('license_pre_routing_factory', () => { describe('#reportingFeaturePreRoutingFactory', () => { @@ -27,13 +28,6 @@ describe('license_pre_routing_factory', () => { }; }); - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - describe('status is not valid', () => { beforeEach(() => { mockLicenseCheckResults = { @@ -42,13 +36,10 @@ describe('license_pre_routing_factory', () => { }); it ('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); + const licensePreRouting = licensePreRoutingFactory(mockServer, () => {}); const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException((response) => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); + const response = licensePreRouting({}, stubRequest, kibanaResponseFactory); + expect(response.status).to.be(403); }); }); @@ -60,9 +51,9 @@ describe('license_pre_routing_factory', () => { }); it ('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); + const licensePreRouting = licensePreRoutingFactory(mockServer, () => null); const stubRequest = {}; - const response = licensePreRouting(stubRequest); + const response = licensePreRouting({}, stubRequest, kibanaResponseFactory); expect(response).to.be(null); }); }); diff --git a/x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/license_pre_routing_factory/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 00000000000000..d2f49672461047 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -0,0 +1,43 @@ +/* + * 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 { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; +import { PLUGIN } from '../../../../common/constants'; +import { LICENSE_STATUS_VALID } from '../../../../../../common/constants/license_status'; +import { ServerShim } from '../../types'; + +export const licensePreRoutingFactory = ( + server: ServerShim, + handler: RequestHandler +): RequestHandler => { + const xpackMainPlugin = server.plugins.xpack_main; + + // License checking and enable/disable logic + return function licensePreRouting( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + const { status } = licenseCheckResults; + + if (status !== LICENSE_STATUS_VALID) { + return response.customError({ + body: { + message: licenseCheckResults.messsage, + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; +}; diff --git a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js rename to x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts index 65f2867662bddb..39e82e7db89642 100644 --- a/x-pack/legacy/plugins/watcher/server/lib/normalized_field_types/normalized_field_types.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/lib/normalized_field_types/normalized_field_types.ts @@ -5,12 +5,12 @@ */ export const normalizedFieldTypes = { - 'long': 'number', - 'integer': 'number', - 'short': 'number', - 'byte': 'number', - 'double': 'number', - 'float': 'number', - 'half_float': 'number', - 'scaled_float': 'number' + long: 'number', + integer: 'number', + short: 'number', + byte: 'number', + double: 'number', + float: 'number', + half_float: 'number', + scaled_float: 'number', }; diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js index 456768c8c02ec0..430669ab26c50c 100644 --- a/x-pack/legacy/plugins/watcher/server/models/action_status/__tests__/action_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/__tests__/action_status.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { ActionStatus } from '../action_status'; -import { ACTION_STATES } from '../../../../common/constants'; +import { ACTION_STATES } from '../../../../../common/constants'; import moment from 'moment'; describe('action_status', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js index eeedf9aefe5f66..7f724cf68211fe 100644 --- a/x-pack/legacy/plugins/watcher/server/models/action_status/action_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/action_status.js @@ -6,8 +6,8 @@ import { get } from 'lodash'; import { badImplementation, badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; -import { ACTION_STATES } from '../../../common/constants'; +import { getMoment } from '../../../../common/lib/get_moment'; +import { ACTION_STATES } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; export class ActionStatus { diff --git a/x-pack/legacy/plugins/watcher/server/models/action_status/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/action_status/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/action_status/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/__tests__/execute_details.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/__tests__/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/__tests__/execute_details.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/__tests__/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/execute_details.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/execute_details.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/execute_details.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/execute_details.js diff --git a/x-pack/legacy/plugins/watcher/server/models/execute_details/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/execute_details/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/execute_details/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/__tests__/fields.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/__tests__/fields.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/__tests__/fields.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/__tests__/fields.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/fields.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/fields.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/fields.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/fields.js diff --git a/x-pack/legacy/plugins/watcher/server/models/fields/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/fields/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/fields/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/fields/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/__tests__/settings.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/__tests__/settings.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/settings/__tests__/settings.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/__tests__/settings.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/settings/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/settings/settings.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/settings/settings.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js index 95a1db7533f418..55622117efedfc 100644 --- a/x-pack/legacy/plugins/watcher/server/models/settings/settings.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/settings/settings.js @@ -5,7 +5,7 @@ */ import { merge } from 'lodash'; -import { ACTION_TYPES } from '../../../common/constants'; +import { ACTION_TYPES } from '../../../../common/constants'; function isEnabledByDefault(actionType) { switch (actionType) { diff --git a/x-pack/legacy/plugins/watcher/server/models/visualize_options/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/visualize_options/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/visualize_options/visualize_options.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/visualize_options.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/visualize_options/visualize_options.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/visualize_options/visualize_options.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js index f96274594872ad..6a6df7d6f7f744 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.js @@ -6,7 +6,7 @@ import { get, map, pick } from 'lodash'; import { badRequest } from 'boom'; -import { Action } from '../../../common/models/action'; +import { Action } from '../../../../common/models/action'; import { WatchStatus } from '../watch_status'; import { i18n } from '@kbn/i18n'; import { WatchErrors } from '../watch_errors'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/base_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/base_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/base_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js similarity index 93% rename from x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js index e319cc1bc277b5..0b011ca33a76b0 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.js @@ -6,8 +6,8 @@ import { isEmpty, cloneDeep, has, merge } from 'lodash'; import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; -import { serializeJsonWatch } from '../../../common/lib/serialization'; +import { WATCH_TYPES } from '../../../../common/constants'; +import { serializeJsonWatch } from '../../../../common/lib/serialization'; export class JsonWatch extends BaseWatch { // This constructor should not be used directly. diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/json_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/json_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/json_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js similarity index 88% rename from x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js index 2bdd03e23c6dcd..72c725eda2bd18 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/get_watch_type.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/get_watch_type.js @@ -5,7 +5,7 @@ */ import { get, contains, values } from 'lodash'; -import { WATCH_TYPES } from '../../../../../common/constants'; +import { WATCH_TYPES } from '../../../../../../common/constants'; export function getWatchType(watchJson) { const type = get(watchJson, 'metadata.xpack.type'); diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/lib/get_watch_type/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/lib/get_watch_type/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js index 977c62726a038e..7f29d41b20fb3b 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.js @@ -7,7 +7,7 @@ import { merge } from 'lodash'; import { badRequest } from 'boom'; import { BaseWatch } from './base_watch'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; export class MonitoringWatch extends BaseWatch { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/monitoring_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/monitoring_watch.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js index 04239ab6e1b5ff..a7524bcc7c4dbe 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/__tests__/format_visualize_data.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/__tests__/format_visualize_data.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { AGG_TYPES } from '../../../../../common/constants'; +import { AGG_TYPES } from '../../../../../../common/constants'; import { formatVisualizeData } from '../format_visualize_data'; describe('watch', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js similarity index 95% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js index ab9daf6f636a1a..c3b73d23d96b10 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/build_visualize_query.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/build_visualize_query.js @@ -5,8 +5,8 @@ */ import { cloneDeep } from 'lodash'; -import { buildInput } from '../../../../common/lib/serialization'; -import { AGG_TYPES } from '../../../../common/constants'; +import { buildInput } from '../../../../../common/lib/serialization'; +import { AGG_TYPES } from '../../../../../common/constants'; /* input.search.request.body.query.bool.filter.range diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/count_terms.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/count_terms.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.date.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.json b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.json similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/data_samples/non_count_terms.query.json rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/data_samples/non_count_terms.query.json diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js index 90cdc9464e8c58..19d41d2491cf54 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/format_visualize_data.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/format_visualize_data.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AGG_TYPES } from '../../../../common/constants'; +import { AGG_TYPES } from '../../../../../common/constants'; export function formatVisualizeData({ aggType, termField }, results) { if (aggType === AGG_TYPES.COUNT && !Boolean(termField)) { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js index cb40c46ac64356..db662902d0f4d2 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.js @@ -6,8 +6,8 @@ import { merge } from 'lodash'; import { BaseWatch } from '../base_watch'; -import { WATCH_TYPES, COMPARATORS, SORT_ORDERS } from '../../../../common/constants'; -import { serializeThresholdWatch } from '../../../../common/lib/serialization'; +import { WATCH_TYPES, COMPARATORS, SORT_ORDERS } from '../../../../../common/constants'; +import { serializeThresholdWatch } from '../../../../../common/lib/serialization'; import { buildVisualizeQuery } from './build_visualize_query'; import { formatVisualizeData } from './format_visualize_data'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js index 4a0b7b657bbc6d..6226a702d7f3c5 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/threshold_watch/threshold_watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/threshold_watch/threshold_watch.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { COMPARATORS, SORT_ORDERS } from '../../../../common/constants'; +import { COMPARATORS, SORT_ORDERS } from '../../../../../common/constants'; import { WatchErrors } from '../../watch_errors'; import { ThresholdWatch } from './threshold_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/watch.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch/watch.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js index c75afc62c4c4bc..10b021dcbedf6d 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/watch.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.js @@ -6,7 +6,7 @@ import { set } from 'lodash'; import { badRequest } from 'boom'; -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { JsonWatch } from './json_watch'; import { MonitoringWatch } from './monitoring_watch'; import { ThresholdWatch } from './threshold_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js index 2895c23083def1..c419c285617308 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch/watch.test.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch/watch.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { WATCH_TYPES } from '../../../common/constants'; +import { WATCH_TYPES } from '../../../../common/constants'; import { Watch } from './watch'; import { JsonWatch } from './json_watch'; import { MonitoringWatch } from './monitoring_watch'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.test.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_errors/watch_errors.test.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_errors/watch_errors.test.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/__tests__/watch_history_item.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/__tests__/watch_history_item.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/__tests__/watch_history_item.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js similarity index 97% rename from x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js index 617f7585717426..5172e590fc63e4 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_history_item/watch_history_item.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_history_item/watch_history_item.js @@ -5,7 +5,7 @@ */ import { badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../common/lib/get_moment'; import { get, cloneDeep } from 'lodash'; import { WatchStatus } from '../watch_status'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js similarity index 99% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js index e29c8dd2a529e3..9a045fa4b5a7f4 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_status/__tests__/watch_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/__tests__/watch_status.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { WatchStatus } from '../watch_status'; -import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../common/constants'; +import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../../common/constants'; import moment from 'moment'; describe('watch_status', () => { diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/index.js similarity index 100% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/index.js diff --git a/x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js similarity index 98% rename from x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js rename to x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js index b7cffe16ca0bc2..1e3d1d3064cb49 100644 --- a/x-pack/legacy/plugins/watcher/server/models/watch_status/watch_status.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/models/watch_status/watch_status.js @@ -6,9 +6,9 @@ import { get, map, forEach, max } from 'lodash'; import { badRequest } from 'boom'; -import { getMoment } from '../../../common/lib/get_moment'; +import { getMoment } from '../../../../common/lib/get_moment'; import { ActionStatus } from '../action_status'; -import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../common/constants'; +import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; function getActionStatusTotals(watchStatus) { diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts new file mode 100644 index 00000000000000..2e8c81efa19c01 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/plugin.ts @@ -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 { first } from 'rxjs/operators'; +import { Plugin, CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { PLUGIN } from '../../common/constants'; +import { ServerShim, RouteDependencies } from './types'; + +import { registerLicenseChecker } from '../../../../server/lib/register_license_checker'; +import { registerSettingsRoutes } from './routes/api/settings'; +import { registerIndicesRoutes } from './routes/api/indices'; +import { registerLicenseRoutes } from './routes/api/license'; +import { registerWatchesRoutes } from './routes/api/watches'; +import { registerWatchRoutes } from './routes/api/watch'; +import { registerListFieldsRoute } from './routes/api/register_list_fields_route'; +import { registerLoadHistoryRoute } from './routes/api/register_load_history_route'; + +export class WatcherServerPlugin implements Plugin { + async setup( + { http, elasticsearch: elasticsearchService }: CoreSetup, + { __LEGACY: serverShim }: { __LEGACY: ServerShim } + ) { + const elasticsearch = await elasticsearchService.adminClient$.pipe(first()).toPromise(); + const router = http.createRouter(); + const routeDependencies: RouteDependencies = { + elasticsearch, + elasticsearchService, + router, + }; + // Register license checker + registerLicenseChecker( + serverShim as any, + PLUGIN.ID, + PLUGIN.getI18nName(i18n), + PLUGIN.MINIMUM_LICENSE_REQUIRED + ); + + registerListFieldsRoute(routeDependencies, serverShim); + registerLoadHistoryRoute(routeDependencies, serverShim); + registerIndicesRoutes(routeDependencies, serverShim); + registerLicenseRoutes(routeDependencies, serverShim); + registerSettingsRoutes(routeDependencies, serverShim); + registerWatchesRoutes(routeDependencies, serverShim); + registerWatchRoutes(routeDependencies, serverShim); + } + start() {} + stop() {} +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/indices/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts new file mode 100644 index 00000000000000..6b6b643dc4adf1 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_get_route.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { reduce, size } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function getIndexNamesFromAliasesResponse(json: Record) { + return reduce( + json, + (list, { aliases }, indexName) => { + list.push(indexName); + if (size(aliases) > 0) { + list.push(...Object.keys(aliases)); + } + return list; + }, + [] as string[] + ); +} + +function getIndices(callWithRequest: any, pattern: string, limit = 10) { + return callWithRequest('indices.getAlias', { + index: pattern, + ignore: [404], + }).then((aliasResult: any) => { + if (aliasResult.status !== 404) { + const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); + return indicesFromAliasResponse.slice(0, limit); + } + + const params = { + index: pattern, + ignore: [404], + body: { + size: 0, // no hits + aggs: { + indices: { + terms: { + field: '_index', + size: limit, + }, + }, + }, + }, + }; + + return callWithRequest('search', params).then((response: any) => { + if (response.status === 404 || !response.aggregations) { + return []; + } + return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + }); + }); +} + +export function registerGetRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { pattern } = request.body; + + try { + const indices = await getIndices(callWithRequest, pattern); + return response.ok({ body: { indices } }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/indices', + validate: { + body: schema.object({}, { allowUnknowns: true }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts index 41b2f8dba7a1f2..647a85c311532b 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_indices_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/indices/register_indices_routes.ts @@ -5,7 +5,8 @@ */ import { registerGetRoute } from './register_get_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerIndicesRoutes(server) { - registerGetRoute(server); +export function registerIndicesRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerGetRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts index fe890719a0a7d2..c5965d9315b01e 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_license_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_license_routes.ts @@ -5,7 +5,8 @@ */ import { registerRefreshRoute } from './register_refresh_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerLicenseRoutes(server) { - registerRefreshRoute(server); +export function registerLicenseRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerRefreshRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts similarity index 50% rename from x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts index cbd5dc7f6631f4..08f1f26a84a4fb 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/license/register_refresh_route.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/license/register_refresh_route.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; +import { RequestHandler } from 'src/core/server'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; /* In order for the client to have the most up-to-date snapshot of the current license, @@ -12,17 +14,16 @@ it needs to make a round-trip to the kibana server. This refresh endpoint is pro for when the client needs to check the license, but doesn't need to pull data from the server for any reason, i.e., when adding a new watch. */ -export function registerRefreshRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); +export function registerRefreshRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = (ctx, request, response) => { + return response.ok({ body: { success: true } }); + }; - server.route({ - path: '/api/watcher/license/refresh', - method: 'GET', - handler: () => { - return { success: true }; + deps.router.get( + { + path: '/api/watcher/license/refresh', + validate: false, }, - config: { - pre: [ licensePreRouting ] - } - }); + licensePreRoutingFactory(legacy, handler) + ); } diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts new file mode 100644 index 00000000000000..f3222d24f0adf9 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_list_fields_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Fields } from '../../models/fields'; +import { RouteDependencies, ServerShim } from '../../types'; + +function fetchFields(callWithRequest: any, indexes: string[]) { + const params = { + index: indexes, + fields: ['*'], + ignoreUnavailable: true, + allowNoIndices: true, + ignore: 404, + }; + + return callWithRequest('fieldCaps', params); +} + +export function registerListFieldsRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { indexes } = request.body; + + try { + const fieldsResponse = await fetchFields(callWithRequest, indexes); + const json = fieldsResponse.status === 404 ? { fields: [] } : fieldsResponse; + const fields = Fields.fromUpstreamJson(json); + return response.ok({ body: fields.downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/fields', + validate: { + body: schema.object({ + indexes: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.ts new file mode 100644 index 00000000000000..d62e4f48c56296 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/register_load_history_route.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 { schema } from '@kbn/config-schema'; +import { get } from 'lodash'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +import { INDEX_NAMES } from '../../../../common/constants'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../types'; +// @ts-ignore +import { WatchHistoryItem } from '../../models/watch_history_item'; + +function fetchHistoryItem(callWithRequest: any, watchHistoryItemId: string) { + return callWithRequest('search', { + index: INDEX_NAMES.WATCHER_HISTORY, + body: { + query: { + bool: { + must: [{ term: { _id: watchHistoryItemId } }], + }, + }, + }, + }); +} + +export function registerLoadHistoryRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const id = request.params.id; + + try { + const responseFromES = await fetchHistoryItem(callWithRequest, id); + const hit = get(responseFromES, 'hits.hits[0]'); + if (!hit) { + return response.notFound({ body: `Watch History Item with id = ${id} not found` }); + } + const watchHistoryItemJson = get(hit, '_source'); + const watchId = get(hit, '_source.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { watchHistoryItem: watchHistoryItem.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/history/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/settings/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts new file mode 100644 index 00000000000000..710d079d810da8 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_load_route.ts @@ -0,0 +1,43 @@ +/* + * 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 { IClusterClient, RequestHandler } from 'src/core/server'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Settings } from '../../../models/settings'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchClusterSettings(client: IClusterClient) { + return client.callAsInternalUser('cluster.getSettings', { + includeDefaults: true, + filterPath: '**.xpack.notification', + }); +} + +export function registerLoadRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + try { + const settings = await fetchClusterSettings(deps.elasticsearch); + return response.ok({ body: Settings.fromUpstreamJson(settings).downstreamJson }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + deps.router.get( + { + path: '/api/watcher/settings', + validate: false, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts index bef26fbb9b267e..0b24ec0e90bd42 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_history_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/settings/register_settings_routes.ts @@ -5,7 +5,8 @@ */ import { registerLoadRoute } from './register_load_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerHistoryRoutes(server) { - registerLoadRoute(server); +export function registerSettingsRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerLoadRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/action/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts new file mode 100644 index 00000000000000..d0cc0a27e87ff1 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_acknowledge_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { get } from 'lodash'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; +import { isEsError } from '../../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { WatchStatus } from '../../../../models/watch_status'; +import { RouteDependencies, ServerShim } from '../../../../types'; + +function acknowledgeAction(callWithRequest: any, watchId: string, actionId: string) { + return callWithRequest('watcher.ackWatch', { + id: watchId, + action: actionId, + }); +} + +export function registerAcknowledgeRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { watchId, actionId } = request.params; + + try { + const hit = await acknowledgeAction(callWithRequest, watchId, actionId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { watchStatus: watchStatus.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', + validate: { + params: schema.object({ + watchId: schema.string(), + actionId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts similarity index 61% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts index 6f2c86664420ba..022c844867938b 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_action_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/action/register_action_routes.ts @@ -5,7 +5,8 @@ */ import { registerAcknowledgeRoute } from './register_acknowledge_route'; +import { RouteDependencies, ServerShim } from '../../../../types'; -export function registerActionRoutes(server) { - registerAcknowledgeRoute(server); +export function registerActionRoutes(server: RouteDependencies, legacy: ServerShim) { + registerAcknowledgeRoute(server, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts new file mode 100644 index 00000000000000..28c482124aaee0 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_activate_route.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchStatus } from '../../../models/watch_status'; + +function activateWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.activateWatch', { + id: watchId, + }); +} + +export function registerActivateRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + const hit = await activateWatch(callWithRequest, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/activate', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts new file mode 100644 index 00000000000000..ac87066379a20c --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_deactivate_route.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchStatus } from '../../../models/watch_status'; + +function deactivateWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.deactivateWatch', { + id: watchId, + }); +} + +export function registerDeactivateRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + const hit = await deactivateWatch(callWithRequest, watchId); + const watchStatusJson = get(hit, 'status'); + const json = { + id: watchId, + watchStatusJson, + }; + + const watchStatus = WatchStatus.fromUpstreamJson(json); + return response.ok({ + body: { + watchStatus: watchStatus.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{watchId}/deactivate', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts new file mode 100644 index 00000000000000..3402cc283dba04 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.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 { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function deleteWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.deleteWatch', { + id: watchId, + }); +} + +export function registerDeleteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const { watchId } = request.params; + + try { + await deleteWatch(callWithRequest, watchId); + return response.noContent(); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${watchId} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.delete( + { + path: '/api/watcher/watch/{watchId}', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts new file mode 100644 index 00000000000000..f3bce228653fe5 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_execute_route.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; + +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { ExecuteDetails } from '../../../models/execute_details'; +// @ts-ignore +import { Watch } from '../../../models/watch'; +// @ts-ignore +import { WatchHistoryItem } from '../../../models/watch_history_item'; + +function executeWatch(callWithRequest: any, executeDetails: any, watchJson: any) { + const body = executeDetails; + body.watch = watchJson; + + return callWithRequest('watcher.executeWatch', { + body, + }); +} + +export function registerExecuteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const executeDetails = ExecuteDetails.fromDownstreamJson(request.body.executeDetails); + const watch = Watch.fromDownstreamJson(request.body.watch); + + try { + const hit = await executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson); + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, 'watch_record'); + const watchId = get(hit, 'watch_record.watch_id'); + const json = { + id, + watchId, + watchHistoryItemJson, + includeDetails: true, + }; + + const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); + return response.ok({ + body: { + watchHistoryItem: watchHistoryItem.downstreamJson, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/execute', + validate: { + body: schema.object({ + executeDetails: schema.object({}, { allowUnknowns: true }), + watch: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts new file mode 100644 index 00000000000000..e236d7dd642a39 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_history_route.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; +import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../../common/constants'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { WatchHistoryItem } from '../../../models/watch_history_item'; + +function fetchHistoryItems(callWithRequest: any, watchId: any, startTime: any) { + const params: any = { + index: INDEX_NAMES.WATCHER_HISTORY, + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + body: { + size: ES_SCROLL_SETTINGS.PAGE_SIZE, + sort: [{ 'result.execution_time': 'desc' }], + query: { + bool: { + must: [{ term: { watch_id: watchId } }], + }, + }, + }, + }; + + // Add time range clause to query if startTime is specified + if (startTime !== 'all') { + const timeRangeQuery = { range: { 'result.execution_time': { gte: startTime } } }; + params.body.query.bool.must.push(timeRangeQuery); + } + + return callWithRequest('search', params).then((response: any) => + fetchAllFromScroll(response, callWithRequest) + ); +} + +export function registerHistoryRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { watchId } = request.params; + const { startTime } = request.query; + + try { + const hits = await fetchHistoryItems(callWithRequest, watchId, startTime); + const watchHistoryItems = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchHistoryItemJson = get(hit, '_source'); + + const opts = { includeDetails: false }; + return WatchHistoryItem.fromUpstreamJson( + { + id, + watchId, + watchHistoryItemJson, + }, + opts + ); + }); + + return response.ok({ + body: { + watchHistoryItems: watchHistoryItems.map( + (watchHistoryItem: any) => watchHistoryItem.downstreamJson + ), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/watch/{watchId}/history', + validate: { + params: schema.object({ + watchId: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts new file mode 100644 index 00000000000000..7311ad08f73a60 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_load_route.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +// @ts-ignore +import { Watch } from '../../../models/watch'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.getWatch', { + id: watchId, + }); +} + +export function registerLoadRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + const id = request.params.id; + + try { + const hit = await fetchWatch(callWithRequest, id); + const watchJson = get(hit, 'watch'); + const watchStatusJson = get(hit, 'status'); + const json = { + id, + watchJson, + watchStatusJson, + }; + + const watch = Watch.fromUpstreamJson(json, { + throwExceptions: { + Action: false, + }, + }); + return response.ok({ + body: { watch: watch.downstreamJson }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + const body = e.statusCode === 404 ? `Watch with id = ${id} not found` : e; + return response.customError({ statusCode: e.statusCode, body }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + deps.router.get( + { + path: '/api/watcher/watch/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts new file mode 100644 index 00000000000000..5d22392d49ed8a --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; +import { WATCH_TYPES } from '../../../../../common/constants'; +import { + serializeJsonWatch, + serializeThresholdWatch, +} from '../../../../../common/lib/serialization'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function fetchWatch(callWithRequest: any, watchId: string) { + return callWithRequest('watcher.getWatch', { + id: watchId, + }); +} + +function saveWatch(callWithRequest: any, id: string, body: any) { + return callWithRequest('watcher.putWatch', { + id, + body, + }); +} + +export function registerSaveRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const { id } = request.params; + const { type, isNew, ...watchConfig } = request.body; + + // For new watches, verify watch with the same ID doesn't already exist + if (isNew) { + try { + const existingWatch = await fetchWatch(callWithRequest, id); + if (existingWatch.found) { + return response.conflict({ + body: { + message: i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { + defaultMessage: "There is already a watch with ID '{watchId}'.", + values: { + watchId: id, + }, + }), + }, + }); + } + } catch (e) { + const es404 = isEsError(e) && e.statusCode === 404; + if (!es404) { + return response.internalError({ body: e }); + } + // Else continue... + } + } + + let serializedWatch; + + switch (type) { + case WATCH_TYPES.JSON: + const { name, watch } = watchConfig; + serializedWatch = serializeJsonWatch(name, watch); + break; + + case WATCH_TYPES.THRESHOLD: + serializedWatch = serializeThresholdWatch(watchConfig); + break; + } + + try { + // Create new watch + await saveWatch(callWithRequest, id, serializedWatch); + return response.noContent(); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.put( + { + path: '/api/watcher/watch/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({}, { allowUnknowns: true }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts new file mode 100644 index 00000000000000..d07a264b0b2b1f --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_visualize_route.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +// @ts-ignore +import { Watch } from '../../../models/watch'; +// @ts-ignore +import { VisualizeOptions } from '../../../models/visualize_options'; + +function fetchVisualizeData(callWithRequest: any, index: any, body: any) { + const params = { + index, + body, + ignoreUnavailable: true, + allowNoIndices: true, + ignore: [404], + }; + + return callWithRequest('search', params); +} + +export function registerVisualizeRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + const watch = Watch.fromDownstreamJson(request.body.watch); + const options = VisualizeOptions.fromDownstreamJson(request.body.options); + const body = watch.getVisualizeQuery(options); + + try { + const hits = await fetchVisualizeData(callWithRequest, watch.index, body); + const visualizeData = watch.formatVisualizeData(hits); + + return response.ok({ + body: { + visualizeData, + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ statusCode: e.statusCode, body: e }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/watch/visualize', + validate: { + body: schema.object({ + watch: schema.object({}, { allowUnknowns: true }), + options: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts index 8419f6db7f659b..5ecbf3e0d2b46f 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_watch_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_watch_routes.ts @@ -13,15 +13,16 @@ import { registerActivateRoute } from './register_activate_route'; import { registerDeactivateRoute } from './register_deactivate_route'; import { registerVisualizeRoute } from './register_visualize_route'; import { registerActionRoutes } from './action'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerWatchRoutes(server) { - registerDeleteRoute(server); - registerExecuteRoute(server); - registerLoadRoute(server); - registerSaveRoute(server); - registerHistoryRoute(server); - registerActivateRoute(server); - registerDeactivateRoute(server); - registerActionRoutes(server); - registerVisualizeRoute(server); +export function registerWatchRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerDeleteRoute(deps, legacy); + registerExecuteRoute(deps, legacy); + registerLoadRoute(deps, legacy); + registerSaveRoute(deps, legacy); + registerHistoryRoute(deps, legacy); + registerActivateRoute(deps, legacy); + registerDeactivateRoute(deps, legacy); + registerActionRoutes(deps, legacy); + registerVisualizeRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/index.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/index.ts similarity index 100% rename from x-pack/legacy/plugins/watcher/server/routes/api/watches/index.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/index.ts diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts new file mode 100644 index 00000000000000..29c539a0de138a --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_delete_route.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RequestHandler } from 'src/core/server'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; + +function deleteWatches(callWithRequest: any, watchIds: string[]) { + const deletePromises = watchIds.map(watchId => { + return callWithRequest('watcher.deleteWatch', { + id: watchId, + }) + .then((success: Array<{ _id: string }>) => ({ success })) + .catch((error: Array<{ _id: string }>) => ({ error })); + }); + + return Promise.all(deletePromises).then(results => { + const errors: Error[] = []; + const successes: boolean[] = []; + results.forEach(({ success, error }) => { + if (success) { + successes.push(success._id); + } else if (error) { + errors.push(error._id); + } + }); + + return { + successes, + errors, + }; + }); +} + +export function registerDeleteRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const results = await deleteWatches(callWithRequest, request.body.watchIds); + return response.ok({ body: { results } }); + } catch (e) { + return response.internalError({ body: e }); + } + }; + + deps.router.post( + { + path: '/api/watcher/watches/delete', + validate: { + body: schema.object({ + watchIds: schema.arrayOf(schema.string()), + }), + }, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts new file mode 100644 index 00000000000000..b94c29e0f98926 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_list_route.ts @@ -0,0 +1,86 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { get } from 'lodash'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; +import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../../common/constants'; +import { isEsError } from '../../../lib/is_es_error'; +import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; +import { RouteDependencies, ServerShim } from '../../../types'; +// @ts-ignore +import { Watch } from '../../../models/watch'; + +function fetchWatches(callWithRequest: any) { + const params = { + index: INDEX_NAMES.WATCHES, + scroll: ES_SCROLL_SETTINGS.KEEPALIVE, + body: { + size: ES_SCROLL_SETTINGS.PAGE_SIZE, + }, + ignore: [404], + }; + + return callWithRequest('search', params).then((response: any) => + fetchAllFromScroll(response, callWithRequest) + ); +} + +export function registerListRoute(deps: RouteDependencies, legacy: ServerShim) { + const handler: RequestHandler = async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); + + try { + const hits = await fetchWatches(callWithRequest); + const watches = hits.map((hit: any) => { + const id = get(hit, '_id'); + const watchJson = get(hit, '_source'); + const watchStatusJson = get(hit, '_source.status'); + + return Watch.fromUpstreamJson( + { + id, + watchJson, + watchStatusJson, + }, + { + throwExceptions: { + Action: false, + }, + } + ); + }); + + return response.ok({ + body: { + watches: watches.map((watch: any) => watch.downstreamJson), + }, + }); + } catch (e) { + // Case: Error from Elasticsearch JS client + if (isEsError(e)) { + return response.customError({ + statusCode: e.statusCode, + body: { + message: e.message, + }, + }); + } + + // Case: default + return response.internalError({ body: e }); + } + }; + + deps.router.get( + { + path: '/api/watcher/watches', + validate: false, + }, + licensePreRoutingFactory(legacy, handler) + ); +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts similarity index 62% rename from x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js rename to x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts index 5f7ae6a5935bda..dd5f55078e5913 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_watches_routes.js +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watches/register_watches_routes.ts @@ -6,8 +6,9 @@ import { registerListRoute } from './register_list_route'; import { registerDeleteRoute } from './register_delete_route'; +import { RouteDependencies, ServerShim } from '../../../types'; -export function registerWatchesRoutes(server) { - registerListRoute(server); - registerDeleteRoute(server); +export function registerWatchesRoutes(deps: RouteDependencies, legacy: ServerShim) { + registerListRoute(deps, legacy); + registerDeleteRoute(deps, legacy); } diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/types.ts b/x-pack/legacy/plugins/watcher/server/np_ready/types.ts new file mode 100644 index 00000000000000..3d4454aa2a8b77 --- /dev/null +++ b/x-pack/legacy/plugins/watcher/server/np_ready/types.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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; +import { XPackMainPlugin } from '../../../xpack_main/server/xpack_main'; + +export interface ServerShim { + route: any; + plugins: { + xpack_main: XPackMainPlugin; + watcher: any; + }; +} + +export interface RouteDependencies { + router: IRouter; + elasticsearchService: ElasticsearchServiceSetup; + elasticsearch: IClusterClient; +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js deleted file mode 100644 index 7d45d3a2aa60b4..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/register_list_route.js +++ /dev/null @@ -1,60 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; -import { Fields } from '../../../models/fields'; - -function fetchFields(callWithRequest, indexes) { - const params = { - index: indexes, - fields: ['*'], - ignoreUnavailable: true, - allowNoIndices: true, - ignore: 404 - }; - - return callWithRequest('fieldCaps', params); -} - -export function registerListRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/fields', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { indexes } = request.payload; - - return fetchFields(callWithRequest, indexes) - .then(response => { - const json = (response.status === 404) - ? { fields: [] } - : response; - - const fields = Fields.fromUpstreamJson(json); - - return fields.downstreamJson; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js b/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js deleted file mode 100644 index 9a66353c742bc0..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { registerHistoryRoutes } from './register_history_routes'; diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js deleted file mode 100644 index 1d34be56fcefce..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/history/register_load_route.js +++ /dev/null @@ -1,78 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError, wrapCustomError } from '../../../lib/error_wrappers'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchHistoryItem(callWithRequest, watchHistoryItemId) { - return callWithRequest('search', { - index: INDEX_NAMES.WATCHER_HISTORY, - body: { - query: { - bool: { - must: [ - { term: { '_id': watchHistoryItemId } }, - ] - } - } - } - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/history/{id}', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const id = request.params.id; - - return fetchHistoryItem(callWithRequest, id) - .then((responseFromES) => { - const hit = get(responseFromES, 'hits.hits[0]'); - if (!hit) { - throw wrapCustomError( - new Error(`Watch History Item with id = ${id} not found`), 404 - ); - } - - const watchHistoryItemJson = get(hit, '_source'); - const watchId = get(hit, '_source.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return { - watchHistoryItem: watchHistoryItem.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js deleted file mode 100644 index 86de6f3da7ad52..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/indices/register_get_route.js +++ /dev/null @@ -1,89 +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 { reduce, size } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function getIndexNamesFromAliasesResponse(json) { - return reduce(json, (list, { aliases }, indexName) => { - list.push(indexName); - if (size(aliases) > 0) { - list.push(...Object.keys(aliases)); - } - return list; - }, []); -} - -function getIndices(callWithRequest, pattern, limit = 10) { - return callWithRequest('indices.getAlias', { - index: pattern, - ignore: [404] - }) - .then(aliasResult => { - if (aliasResult.status !== 404) { - const indicesFromAliasResponse = getIndexNamesFromAliasesResponse(aliasResult); - return indicesFromAliasResponse.slice(0, limit); - } - - const params = { - index: pattern, - ignore: [404], - body: { - size: 0, // no hits - aggs: { - indices: { - terms: { - field: '_index', - size: limit, - } - } - } - } - }; - - return callWithRequest('search', params) - .then(response => { - if (response.status === 404 || !response.aggregations) { - return []; - } - return response.aggregations.indices.buckets.map(bucket => bucket.key); - }); - }); -} - -export function registerGetRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/indices', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { pattern } = request.payload; - - return getIndices(callWithRequest, pattern) - .then(indices => { - return { indices }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js deleted file mode 100644 index 65c961c8c82f2c..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/settings/register_load_route.js +++ /dev/null @@ -1,47 +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 { callWithInternalUserFactory } from '../../../lib/call_with_internal_user_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { Settings } from '../../../models/settings'; - -function fetchClusterSettings(callWithInternalUser) { - return callWithInternalUser('cluster.getSettings', { - includeDefaults: true, - filterPath: '**.xpack.notification' - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - const callWithInternalUser = callWithInternalUserFactory(server); - - server.route({ - path: '/api/watcher/settings', - method: 'GET', - handler: () => { - return fetchClusterSettings(callWithInternalUser) - .then((settings) => { - return Settings.fromUpstreamJson(settings).downstreamJson; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js deleted file mode 100644 index ffecebf805cf6b..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/action/register_acknowledge_route.js +++ /dev/null @@ -1,63 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../../lib/error_wrappers'; -import { WatchStatus } from '../../../../models/watch_status'; -import { licensePreRoutingFactory } from'../../../../lib/license_pre_routing_factory'; - -export function registerAcknowledgeRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/action/{actionId}/acknowledge', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { watchId, actionId } = request.params; - - return acknowledgeAction(callWithRequest, watchId, actionId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} - -function acknowledgeAction(callWithRequest, watchId, actionId) { - return callWithRequest('watcher.ackWatch', { - id: watchId, - action: actionId - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js deleted file mode 100644 index ea669a16a01722..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_activate_route.js +++ /dev/null @@ -1,63 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { WatchStatus } from '../../../models/watch_status'; - -function activateWatch(callWithRequest, watchId) { - return callWithRequest('watcher.activateWatch', { - id: watchId - }); -} - -export function registerActivateRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/activate', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return activateWatch(callWithRequest, watchId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js deleted file mode 100644 index 2411290e2034a9..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_deactivate_route.js +++ /dev/null @@ -1,63 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { WatchStatus } from '../../../models/watch_status'; - -function deactivateWatch(callWithRequest, watchId) { - return callWithRequest('watcher.deactivateWatch', { - id: watchId - }); -} - -export function registerDeactivateRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/deactivate', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return deactivateWatch(callWithRequest, watchId) - .then(hit => { - const watchStatusJson = get(hit, 'status'); - const json = { - id: watchId, - watchStatusJson: watchStatusJson - }; - - const watchStatus = WatchStatus.fromUpstreamJson(json); - return { - watchStatus: watchStatus.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js deleted file mode 100644 index dc3b015dffa907..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_delete_route.js +++ /dev/null @@ -1,50 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function deleteWatch(callWithRequest, watchId) { - return callWithRequest('watcher.deleteWatch', { - id: watchId - }); -} - -export function registerDeleteRoute(server) { - - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}', - method: 'DELETE', - handler: (request, h) => { - const callWithRequest = callWithRequestFactory(server, request); - - const { watchId } = request.params; - - return deleteWatch(callWithRequest, watchId) - .then(() => h.response().code(204)) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${watchId} not found` - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js deleted file mode 100644 index f378829147280b..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_execute_route.js +++ /dev/null @@ -1,69 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { ExecuteDetails } from '../../../models/execute_details'; -import { Watch } from '../../../models/watch'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function executeWatch(callWithRequest, executeDetails, watchJson) { - const body = executeDetails; - body.watch = watchJson; - - return callWithRequest('watcher.executeWatch', { - body - }); -} - -export function registerExecuteRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/execute', - method: 'PUT', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const executeDetails = ExecuteDetails.fromDownstreamJson(request.payload.executeDetails); - const watch = Watch.fromDownstreamJson(request.payload.watch); - - return executeWatch(callWithRequest, executeDetails.upstreamJson, watch.watchJson) - .then((hit) => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, 'watch_record'); - const watchId = get(hit, 'watch_record.watch_id'); - const json = { - id, - watchId, - watchHistoryItemJson, - includeDetails: true - }; - - const watchHistoryItem = WatchHistoryItem.fromUpstreamJson(json); - return { - watchHistoryItem: watchHistoryItem.downstreamJson - }; - }) - .catch(err => { - - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js deleted file mode 100644 index 702cf8a2b64e27..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_history_route.js +++ /dev/null @@ -1,89 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; -import { WatchHistoryItem } from '../../../models/watch_history_item'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchHistoryItems(callWithRequest, watchId, startTime) { - const params = { - index: INDEX_NAMES.WATCHER_HISTORY, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - sort: [ - { 'result.execution_time': 'desc' } - ], - query: { - bool: { - must: [ - { term: { 'watch_id': watchId } }, - ] - } - } - } - }; - - // Add time range clause to query if startTime is specified - if (startTime !== 'all') { - const timeRangeQuery = { range: { 'result.execution_time': { gte: startTime } } }; - params.body.query.bool.must.push(timeRangeQuery); - } - - return callWithRequest('search', params) - .then(response => fetchAllFromScroll(response, callWithRequest)); -} - -export function registerHistoryRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{watchId}/history', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { watchId } = request.params; - const { startTime } = request.query; - - return fetchHistoryItems(callWithRequest, watchId, startTime) - .then(hits => { - const watchHistoryItems = hits.map(hit => { - const id = get(hit, '_id'); - const watchHistoryItemJson = get(hit, '_source'); - - const opts = { includeDetails: false }; - return WatchHistoryItem.fromUpstreamJson({ - id, - watchId, - watchHistoryItemJson - }, opts); - }); - - return { - watchHistoryItems: watchHistoryItems.map(watchHistoryItem => watchHistoryItem.downstreamJson) - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js deleted file mode 100644 index e5210dbff35670..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_load_route.js +++ /dev/null @@ -1,68 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { get } from 'lodash'; -import { Watch } from '../../../models/watch'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchWatch(callWithRequest, watchId) { - return callWithRequest('watcher.getWatch', { - id: watchId - }); -} - -export function registerLoadRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{id}', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - const id = request.params.id; - - return fetchWatch(callWithRequest, id) - .then(hit => { - const watchJson = get(hit, 'watch'); - const watchStatusJson = get(hit, 'status'); - const json = { - id, - watchJson, - watchStatusJson, - }; - - const watch = Watch.fromUpstreamJson(json, { - throwExceptions: { - Action: false, - }, - }); - return { - watch: watch.downstreamJson - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - const statusCodeToMessageMap = { - 404: `Watch with id = ${id} not found`, - }; - throw wrapEsError(err, statusCodeToMessageMap); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js deleted file mode 100644 index 3cbb0a4e1cc47a..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_save_route.js +++ /dev/null @@ -1,94 +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 { WATCH_TYPES } from '../../../../common/constants'; -import { serializeJsonWatch, serializeThresholdWatch } from '../../../../common/lib/serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError, wrapCustomError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; -import { i18n } from '@kbn/i18n'; - -function fetchWatch(callWithRequest, watchId) { - return callWithRequest('watcher.getWatch', { - id: watchId - }); -} - -function saveWatch(callWithRequest, id, body) { - return callWithRequest('watcher.putWatch', { - id, - body, - }); -} - -export function registerSaveRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/{id}', - method: 'PUT', - handler: async (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const { id, type, isNew, ...watchConfig } = request.payload; - - // For new watches, verify watch with the same ID doesn't already exist - if (isNew) { - const conflictError = wrapCustomError( - new Error(i18n.translate('xpack.watcher.saveRoute.duplicateWatchIdErrorMessage', { - defaultMessage: 'There is already a watch with ID \'{watchId}\'.', - values: { - watchId: id, - } - })), - 409 - ); - - try { - const existingWatch = await fetchWatch(callWithRequest, id); - - if (existingWatch.found) { - throw conflictError; - } - } catch (e) { - // Rethrow conflict error but silently swallow all others - if (e === conflictError) { - throw e; - } - } - } - - let serializedWatch; - - switch (type) { - case WATCH_TYPES.JSON: - const { name, watch } = watchConfig; - serializedWatch = serializeJsonWatch(name, watch); - break; - - case WATCH_TYPES.THRESHOLD: - serializedWatch = serializeThresholdWatch(watchConfig); - break; - } - - // Create new watch - return saveWatch(callWithRequest, id, serializedWatch) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js deleted file mode 100644 index ff9d8f9775d5ed..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watch/register_visualize_route.js +++ /dev/null @@ -1,62 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { Watch } from '../../../models/watch'; -import { VisualizeOptions } from '../../../models/visualize_options'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchVisualizeData(callWithRequest, index, body) { - const params = { - index, - body, - ignoreUnavailable: true, - allowNoIndices: true, - ignore: [404] - }; - - return callWithRequest('search', params); -} - -export function registerVisualizeRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watch/visualize', - method: 'POST', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - const watch = Watch.fromDownstreamJson(request.payload.watch); - const options = VisualizeOptions.fromDownstreamJson(request.payload.options); - const body = watch.getVisualizeQuery(options); - - return fetchVisualizeData(callWithRequest, watch.index, body) - .then(hits => { - const visualizeData = watch.formatVisualizeData(hits); - - return { - visualizeData - }; - }) - .catch(err => { - - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js deleted file mode 100644 index a0bbfb954b755a..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_delete_route.js +++ /dev/null @@ -1,58 +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 { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../../lib/license_pre_routing_factory'; - -function deleteWatches(callWithRequest, watchIds) { - const deletePromises = watchIds.map(watchId => { - return callWithRequest('watcher.deleteWatch', { - id: watchId, - }) - .then(success => ({ success })) - .catch(error => ({ error })); - }); - - return Promise.all(deletePromises).then(results => { - const errors = []; - const successes = []; - results.forEach(({ success, error }) => { - if (success) { - successes.push(success._id); - } else if (error) { - errors.push(error._id); - } - }); - - return { - successes, - errors, - }; - }); -} - -export function registerDeleteRoute(server) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watches/delete', - method: 'POST', - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const results = await deleteWatches(callWithRequest, request.payload.watchIds); - return { results }; - } catch (err) { - throw wrapUnknownError(err); - } - }, - config: { - pre: [licensePreRouting], - }, - }); -} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js b/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js deleted file mode 100644 index 2a617e275d1ee6..00000000000000 --- a/x-pack/legacy/plugins/watcher/server/routes/api/watches/register_list_route.js +++ /dev/null @@ -1,79 +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 { get } from 'lodash'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { fetchAllFromScroll } from '../../../lib/fetch_all_from_scroll'; -import { INDEX_NAMES, ES_SCROLL_SETTINGS } from '../../../../common/constants'; -import { Watch } from '../../../models/watch'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../../lib/error_wrappers'; -import { licensePreRoutingFactory } from'../../../lib/license_pre_routing_factory'; - -function fetchWatches(callWithRequest) { - const params = { - index: INDEX_NAMES.WATCHES, - scroll: ES_SCROLL_SETTINGS.KEEPALIVE, - body: { - size: ES_SCROLL_SETTINGS.PAGE_SIZE, - }, - ignore: [404] - }; - - return callWithRequest('search', params) - .then(response => fetchAllFromScroll(response, callWithRequest)); -} - -export function registerListRoute(server) { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: '/api/watcher/watches', - method: 'GET', - handler: (request) => { - const callWithRequest = callWithRequestFactory(server, request); - - return fetchWatches(callWithRequest) - .then(hits => { - const watches = hits.map(hit => { - const id = get(hit, '_id'); - const watchJson = get(hit, '_source'); - const watchStatusJson = get(hit, '_source.status'); - - return Watch.fromUpstreamJson( - { - id, - watchJson, - watchStatusJson, - }, - { - throwExceptions: { - Action: false, - }, - } - ); - }); - - return { - watches: watches.map(watch => watch.downstreamJson) - }; - }) - .catch(err => { - // Case: Error from Elasticsearch JS client - if (isEsError(err)) { - throw wrapEsError(err); - } - - // Case: default - throw wrapUnknownError(err); - }); - }, - config: { - pre: [ licensePreRouting ] - } - }); -} diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 6828833c3f982b..68fea22e4d905f 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -6,9 +6,6 @@ import { resolve } from 'path'; import dedent from 'dedent'; -import { - XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS -} from '../../server/lib/constants'; import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { replaceInjectedVars } from './server/lib/replace_injected_vars'; import { setupXPackMain } from './server/lib/setup_xpack_main'; @@ -34,7 +31,6 @@ export const xpackMain = (kibana) => { enabled: Joi.boolean().default(), url: Joi.string().default(), }).default(), // deprecated - xpack_api_polling_frequency_millis: Joi.number().default(XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS), }).default(); }, @@ -47,6 +43,9 @@ export const xpackMain = (kibana) => { }, uiExports: { + hacks: [ + 'plugins/xpack_main/hacks/check_xpack_info_change', + ], replaceInjectedVars, injectDefaultVars(server) { const config = server.config(); diff --git a/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js b/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js new file mode 100644 index 00000000000000..0de13da68eac61 --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/public/hacks/check_xpack_info_change.js @@ -0,0 +1,53 @@ +/* + * 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 { identity } from 'lodash'; +import { uiModules } from 'ui/modules'; +import { Path } from 'plugins/xpack_main/services/path'; +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { xpackInfoSignature } from 'plugins/xpack_main/services/xpack_info_signature'; + +const module = uiModules.get('xpack_main', []); + +module.factory('checkXPackInfoChange', ($q, Private, $injector) => { + /** + * Intercept each network response to look for the kbn-xpack-sig header. + * When that header is detected, compare its value with the value cached + * in the browser storage. When the value is new, call `xpackInfo.refresh()` + * so that it will pull down the latest x-pack info + * + * @param {object} response - the angular $http response object + * @param {function} handleResponse - callback, expects to receive the response + * @return + */ + function interceptor(response, handleResponse) { + if (Path.isUnauthenticated()) { + return handleResponse(response); + } + + const currentSignature = response.headers('kbn-xpack-sig'); + const cachedSignature = xpackInfoSignature.get(); + + if (currentSignature && cachedSignature !== currentSignature) { + // Signature from the server differ from the signature of our + // cached info, so we need to refresh it. + // Intentionally swallowing this error + // because nothing catches it and it's an ugly console error. + xpackInfo.refresh($injector).catch(() => {}); + } + + return handleResponse(response); + } + + return { + response: (response) => interceptor(response, identity), + responseError: (response) => interceptor(response, $q.reject) + }; +}); + +module.config(($httpProvider) => { + $httpProvider.interceptors.push('checkXPackInfoChange'); +}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index 5b2c6612d2a87c..bd94f951810b09 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { BehaviorSubject } from 'rxjs'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; import { setupXPackMain } from '../setup_xpack_main'; import * as InjectXPackInfoSignatureNS from '../inject_xpack_info_signature'; + describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); @@ -39,7 +41,7 @@ describe('setupXPackMain()', () => { elasticsearch: mockElasticsearchPlugin, xpack_main: mockXPackMainPlugin }, - newPlatform: { setup: { plugins: { features: {} } } }, + newPlatform: { setup: { plugins: { features: {}, licensing: { license$: new BehaviorSubject() } } } }, events: { on() {} }, log() {}, config() {}, @@ -47,9 +49,8 @@ describe('setupXPackMain()', () => { ext() {} }); - // Make sure we don't misspell config key. + // Make sure plugins doesn't consume config const configGetStub = sinon.stub().throws(new Error('`config.get` is called with unexpected key.')); - configGetStub.withArgs('xpack.xpack_main.xpack_api_polling_frequency_millis').returns(1234); mockServer.config.returns({ get: configGetStub }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js index 12426d6a4effb3..52f97fc0cfc3a1 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js @@ -5,36 +5,32 @@ */ import { createHash } from 'crypto'; +import { BehaviorSubject } from 'rxjs'; import expect from '@kbn/expect'; import sinon from 'sinon'; import { XPackInfo } from '../xpack_info'; +import { licensingMock } from '../../../../../../plugins/licensing/server/mocks'; -const nowDate = new Date(2010, 10, 10); - -function getMockXPackInfoAPIResponse(license = {}, features = {}) { - return Promise.resolve({ - build: { - hash: '5927d85', - date: '2010-10-10T00:00:00.000Z' - }, +function createLicense(license = {}, features = {}) { + return licensingMock.createLicense({ license: { uid: 'custom-uid', type: 'gold', mode: 'gold', status: 'active', - expiry_date_in_millis: 1286575200000, + expiryDateInMillis: 1286575200000, ...license }, features: { security: { description: 'Security for the Elastic Stack', - available: true, - enabled: true + isAvailable: true, + isEnabled: true }, watcher: { description: 'Alerting, Notification and Automation for the Elastic Stack', - available: true, - enabled: false + isAvailable: true, + isEnabled: false }, ...features } @@ -48,244 +44,63 @@ function getSignature(object) { } describe('XPackInfo', () => { - const sandbox = sinon.createSandbox(); - let mockServer; - let mockElasticsearchCluster; let mockElasticsearchPlugin; beforeEach(() => { - sandbox.useFakeTimers(nowDate.getTime()); - - mockElasticsearchCluster = { - callWithInternalUser: sinon.stub() - }; - - mockElasticsearchPlugin = { - getCluster: sinon.stub().returns(mockElasticsearchCluster) - }; - mockServer = sinon.stub({ plugins: { elasticsearch: mockElasticsearchPlugin }, events: { on() {} }, - log() { } - }); - }); - - afterEach(() => sandbox.restore()); - - it('correctly initializes its own properties with defaults.', () => { - mockElasticsearchPlugin.getCluster.throws(new Error('`getCluster` is called with unexpected source.')); - mockElasticsearchPlugin.getCluster.withArgs('data').returns(mockElasticsearchCluster); - - const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - // Poller is not started. - sandbox.clock.tick(10000); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - }); + newPlatform: { + setup: { + plugins: { + licensing: { - it('correctly initializes its own properties with custom cluster type.', () => { - mockElasticsearchPlugin.getCluster.throws(new Error('`getCluster` is called with unexpected source.')); - mockElasticsearchPlugin.getCluster.withArgs('monitoring').returns(mockElasticsearchCluster); - - const xPackInfo = new XPackInfo( - mockServer, - { clusterSource: 'monitoring', pollFrequencyInMillis: 1234 } - ); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - // Poller is not started. - sandbox.clock.tick(9999); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); + } + } + } + }, + }); }); describe('refreshNow()', () => { - let xPackInfo; - beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); - }); - - it('forces xpack info to be immediately updated with the data returned from Elasticsearch API.', async () => { - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); - sinon.assert.calledWithExactly(mockElasticsearchCluster.callWithInternalUser, 'transport.request', { - method: 'GET', - path: '/_xpack' + it('delegates to the new platform licensing plugin', async () => { + const refresh = sinon.spy(); + + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$: new BehaviorSubject(createLicense()), + refresh: refresh + } }); - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - }); - - - it('communicates X-Pack being unavailable', async () => { - const badRequestError = new Error('Bad request'); - badRequestError.status = 400; - - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError)); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.isXpackUnavailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be( - 'X-Pack plugin is not installed on the [data] Elasticsearch cluster.' - ); - }); - - it('correctly updates xpack info if Elasticsearch API fails.', async () => { - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(new Error('Uh oh'))); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - }); - - it('correctly updates xpack info when Elasticsearch API recovers after failure.', async () => { - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - expect(xPackInfo.unavailableReason()).to.be(undefined); - - const randomError = new Error('Uh oh'); - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(randomError)); await xPackInfo.refreshNow(); - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be(randomError); - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [data] cluster. ${randomError}` - ); - - const badRequestError = new Error('Bad request'); - badRequestError.status = 400; - mockElasticsearchCluster.callWithInternalUser.returns(Promise.reject(badRequestError)); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(false); - expect(xPackInfo.license.isActive()).to.be(false); - expect(xPackInfo.unavailableReason()).to.be( - 'X-Pack plugin is not installed on the [data] Elasticsearch cluster.' - ); - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [data] cluster. ${badRequestError}` - ); - - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - await xPackInfo.refreshNow(); - - expect(xPackInfo.isAvailable()).to.be(true); - expect(xPackInfo.license.isActive()).to.be(true); - }); - - it('logs license status changes.', async () => { - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'info', 'xpack'], - sinon.match('Imported license information from Elasticsearch for the [data] cluster: ' + - 'mode: gold | status: active | expiry date: ' - ) - ); - mockServer.log.resetHistory(); - - await xPackInfo.refreshNow(); - - // Response is still the same, so nothing should be logged. - sinon.assert.neverCalledWith(mockServer.log, ['license', 'info', 'xpack']); - - // Change mode/status of the license and the change should be logged. - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'expired', mode: 'platinum' }) - ); - - await xPackInfo.refreshNow(); - - sinon.assert.calledWithExactly( - mockServer.log, - ['license', 'info', 'xpack'], - sinon.match('Imported changed license information from Elasticsearch for the [data] cluster: ' + - 'mode: platinum | status: expired | expiry date: ' - ) - ); - }); - - it('restarts the poller.', async () => { - mockElasticsearchCluster.callWithInternalUser.resetHistory(); - - sandbox.clock.tick(1499); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - sandbox.clock.tick(1); - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); - // Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and - // new poller iteration is rescheduled. - await Promise.resolve(); - - sandbox.clock.tick(1500); - sinon.assert.calledTwice(mockElasticsearchCluster.callWithInternalUser); - // Exhaust micro-task queue, to make sure that `callWithInternalUser` is completed and - // new poller iteration is rescheduled. - await Promise.resolve(); - - sandbox.clock.tick(1499); - await xPackInfo.refreshNow(); - mockElasticsearchCluster.callWithInternalUser.resetHistory(); - - // Since poller has been restarted, it should not be called now. - sandbox.clock.tick(1); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - // Here it still shouldn't be called. - sandbox.clock.tick(1498); - sinon.assert.notCalled(mockElasticsearchCluster.callWithInternalUser); - - sandbox.clock.tick(1); - sinon.assert.calledOnce(mockElasticsearchCluster.callWithInternalUser); + sinon.assert.calledOnce(refresh); }); }); describe('license', () => { let xPackInfo; + let license$; beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + license$ = new BehaviorSubject(createLicense()); + xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); }); - it('getUid() shows license uid returned from the backend.', async () => { + it('getUid() shows license uid returned from the license$.', async () => { expect(xPackInfo.license.getUid()).to.be('custom-uid'); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ uid: 'new-custom-uid' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ uid: 'new-custom-uid' })); expect(xPackInfo.license.getUid()).to.be('new-custom-uid'); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ uid: undefined, error: 'error-reason' })); expect(xPackInfo.license.getUid()).to.be(undefined); }); @@ -293,86 +108,46 @@ describe('XPackInfo', () => { it('isActive() is based on the status returned from the backend.', async () => { expect(xPackInfo.license.isActive()).to.be(true); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'expired' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'expired' })); expect(xPackInfo.license.isActive()).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'some other value' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'some other value' })); expect(xPackInfo.license.isActive()).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ status: 'active' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: 'active' })); expect(xPackInfo.license.isActive()).to.be(true); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ status: undefined, error: 'error-reason' })); expect(xPackInfo.license.isActive()).to.be(false); }); it('getExpiryDateInMillis() is based on the value returned from the backend.', async () => { expect(xPackInfo.license.getExpiryDateInMillis()).to.be(1286575200000); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ expiry_date_in_millis: 10203040 }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ expiryDateInMillis: 10203040 })); expect(xPackInfo.license.getExpiryDateInMillis()).to.be(10203040); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ expiryDateInMillis: undefined, error: 'error-reason' })); expect(xPackInfo.license.getExpiryDateInMillis()).to.be(undefined); }); it('getType() is based on the value returned from the backend.', async () => { expect(xPackInfo.license.getType()).to.be('gold'); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'basic' }) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ type: 'basic' })); expect(xPackInfo.license.getType()).to.be('basic'); - mockElasticsearchCluster.callWithInternalUser.returns( - Promise.reject(new Error('Uh oh')) - ); - await xPackInfo.refreshNow(); - + license$.next(createLicense({ type: undefined, error: 'error-reason' })); expect(xPackInfo.license.getType()).to.be(undefined); }); it('isOneOf() correctly determines if current license is presented in the specified list.', async () => { - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ mode: 'gold' }) - ); - await xPackInfo.refreshNow(); - expect(xPackInfo.license.isOneOf('gold')).to.be(true); expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); expect(xPackInfo.license.isOneOf(['platinum', 'basic'])).to.be(false); expect(xPackInfo.license.isOneOf('standard')).to.be(false); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ mode: 'basic' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ mode: 'basic' })); expect(xPackInfo.license.isOneOf('basic')).to.be(true); expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); @@ -383,18 +158,20 @@ describe('XPackInfo', () => { describe('feature', () => { let xPackInfo; + let license$; beforeEach(async () => { - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({}, { - feature: { - available: false, - enabled: true - } - }) - ); - - xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + license$ = new BehaviorSubject(createLicense({}, { + feature: { + isAvailable: false, + isEnabled: true + } + })); + xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); }); it('isAvailable() checks whether particular feature is available.', async () => { @@ -462,10 +239,7 @@ describe('XPackInfo', () => { someAnotherCustomValue: 500100 }); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum' }) - ); - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum' })); expect(xPackInfo.toJSON().features.security).to.eql({ isXPackInfo: true, @@ -520,10 +294,8 @@ describe('XPackInfo', () => { someAnotherCustomValue: 500100 }); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum' }) - ); - await xPackInfo.refreshNow(); + + license$.next(createLicense({ type: 'platinum' })); expect(securityFeature.getLicenseCheckResults()).to.eql({ isXPackInfo: true, @@ -539,9 +311,13 @@ describe('XPackInfo', () => { }); it('getSignature() returns correct signature.', async () => { - mockElasticsearchCluster.callWithInternalUser.returns(getMockXPackInfoAPIResponse()); - const xPackInfo = new XPackInfo(mockServer, { pollFrequencyInMillis: 1500 }); - await xPackInfo.refreshNow(); + const license$ = new BehaviorSubject(createLicense()); + const xPackInfo = new XPackInfo(mockServer, { + licensing: { + license$, + refresh: () => null + } + }); expect(xPackInfo.getSignature()).to.be(getSignature({ license: { @@ -552,24 +328,21 @@ describe('XPackInfo', () => { features: {} })); - mockElasticsearchCluster.callWithInternalUser.returns( - getMockXPackInfoAPIResponse({ type: 'platinum', expiry_date_in_millis: nowDate.getTime() }) - ); - - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); const expectedSignature = getSignature({ license: { type: 'platinum', isActive: true, - expiryDateInMillis: nowDate.getTime() + expiryDateInMillis: 20304050 }, features: {} }); expect(xPackInfo.getSignature()).to.be(expectedSignature); // Should stay the same after refresh if nothing changed. - await xPackInfo.refreshNow(); + license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); + expect(xPackInfo.getSignature()).to.be(expectedSignature); }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 0155def677c2a6..03e629a18e57ea 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -16,12 +16,16 @@ import { XPackInfo } from './xpack_info'; * @param server {Object} The Kibana server object. */ export function setupXPackMain(server) { - const info = new XPackInfo(server, { - pollFrequencyInMillis: server.config().get('xpack.xpack_main.xpack_api_polling_frequency_millis') - }); + const info = new XPackInfo(server, { licensing: server.newPlatform.setup.plugins.licensing }); server.expose('info', info); - server.expose('createXPackInfo', (options) => new XPackInfo(server, options)); + server.expose('createXPackInfo', (options) => { + const client = server.newPlatform.setup.core.elasticsearch.createClient(options.clusterSource); + const monitoringLicensing = server.newPlatform.setup.plugins.licensing.createLicensePoller(client, options.pollFrequencyInMillis); + + return new XPackInfo(server, { licensing: monitoringLicensing }); + }); + server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); const { registerFeature, getFeatures } = server.newPlatform.setup.plugins.features; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts deleted file mode 100644 index ed7e5be3a8e90a..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Server } from 'hapi'; -import { XPackInfoLicense } from './xpack_info_license'; - -interface XPackFeature { - isAvailable(): boolean; - isEnabled(): boolean; - registerLicenseCheckResultsGenerator(generator: (xpackInfo: XPackInfo) => void): void; - getLicenseCheckResults(): any; -} - -export interface XPackInfoOptions { - clusterSource?: string; - pollFrequencyInMillis: number; -} - -export declare class XPackInfo { - public license: XPackInfoLicense; - - constructor(server: Server, options: XPackInfoOptions); - - public isAvailable(): boolean; - public isXpackUnavailable(): boolean; - public unavailableReason(): string | Error; - public onLicenseInfoChange(handler: () => void): void; - public refreshNow(): Promise; - - public feature(name: string): XPackFeature; - - public getSignature(): string; - public toJSON(): any; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js deleted file mode 100644 index c0e4c779ba591b..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.js +++ /dev/null @@ -1,308 +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 { createHash } from 'crypto'; -import moment from 'moment'; -import { get, has } from 'lodash'; -import { Poller } from '../../../../common/poller'; -import { XPackInfoLicense } from './xpack_info_license'; - -/** - * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. - */ -export class XPackInfo { - /** - * XPack License object. - * @type {XPackInfoLicense} - * @private - */ - _license; - - /** - * Feature name <-> feature license check generator function mapping. - * @type {Map} - * @private - */ - _featureLicenseCheckResultsGenerators = new Map(); - - - /** - * Set of listener functions that will be called whenever the license - * info changes - * @type {Set} - */ - _licenseInfoChangedListeners = new Set(); - - - /** - * Cache that may contain last xpack info API response or error, json representation - * of xpack info and xpack info signature. - * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} - * @private - */ - _cache = {}; - - /** - * XPack info poller. - * @type {Poller} - * @private - */ - _poller; - - /** - * XPack License instance. - * @returns {XPackInfoLicense} - */ - get license() { - return this._license; - } - - /** - * Constructs XPack info object. - * @param {Hapi.Server} server HapiJS server instance. - * @param {Object} options - * @property {string} [options.clusterSource] Type of the cluster that should be used - * to fetch XPack info (data, monitoring etc.). If not provided, `data` is used. - * @property {number} options.pollFrequencyInMillis Polling interval used to automatically - * refresh XPack Info by the internal poller. - */ - constructor(server, { clusterSource = 'data', pollFrequencyInMillis }) { - this._log = server.log.bind(server); - this._cluster = server.plugins.elasticsearch.getCluster(clusterSource); - this._clusterSource = clusterSource; - - // Create a poller that will be (re)started inside of the `refreshNow` call. - this._poller = new Poller({ - functionToPoll: () => this.refreshNow(), - trailing: true, - pollFrequencyInMillis, - continuePollingOnError: true - }); - - server.events.on('stop', () => { - this._poller.stop(); - }); - - this._license = new XPackInfoLicense( - () => this._cache.response && this._cache.response.license - ); - } - - /** - * Checks whether XPack info is available. - * @returns {boolean} - */ - isAvailable() { - return !!this._cache.response && !!this._cache.response.license; - } - - /** - * Checks whether ES was available - * @returns {boolean} - */ - isXpackUnavailable() { - return this._cache.error instanceof Error && this._cache.error.status === 400; - } - - /** - * If present, describes the reason why XPack info is not available. - * @returns {Error|string} - */ - unavailableReason() { - if (!this._cache.error && this._cache.response && !this._cache.response.license) { - return `[${this._clusterSource}] Elasticsearch cluster did not respond with license information.`; - } - - if (this.isXpackUnavailable()) { - return `X-Pack plugin is not installed on the [${this._clusterSource}] Elasticsearch cluster.`; - } - - return this._cache.error; - } - - onLicenseInfoChange(handler) { - this._licenseInfoChangedListeners.add(handler); - } - - /** - * Queries server to get the updated XPack info. - * @returns {Promise.} - */ - async refreshNow() { - this._log(['license', 'debug', 'xpack'], ( - `Calling [${this._clusterSource}] Elasticsearch _xpack API. Polling frequency: ${this._poller.getPollFrequency()}` - )); - - // We can reset polling timer since we force refresh here. - this._poller.stop(); - - try { - const response = await this._cluster.callWithInternalUser('transport.request', { - method: 'GET', - path: '/_xpack' - }); - - const licenseInfoChanged = this._hasLicenseInfoChanged(response); - - if (licenseInfoChanged) { - const licenseInfoParts = [ - `mode: ${get(response, 'license.mode')}`, - `status: ${get(response, 'license.status')}`, - ]; - - if (has(response, 'license.expiry_date_in_millis')) { - const expiryDate = moment(response.license.expiry_date_in_millis, 'x').format(); - licenseInfoParts.push(`expiry date: ${expiryDate}`); - } - - const licenseInfo = licenseInfoParts.join(' | '); - - this._log( - ['license', 'info', 'xpack'], - `Imported ${this._cache.response ? 'changed ' : ''}license information` + - ` from Elasticsearch for the [${this._clusterSource}] cluster: ${licenseInfo}` - ); - } - - this._cache = { response }; - - if (licenseInfoChanged) { - // call license info changed listeners - for (const listener of this._licenseInfoChangedListeners) { - listener(); - } - } - - } catch(error) { - this._log( - ['license', 'warning', 'xpack'], - `License information from the X-Pack plugin could not be obtained from Elasticsearch` + - ` for the [${this._clusterSource}] cluster. ${error}` - ); - - this._cache = { error }; - } - - this._poller.start(); - - return this; - } - - /** - * Returns a wrapper around XPack info that gives an access to the properties of - * the specific feature. - * @param {string} name Name of the feature to get a wrapper for. - * @returns {Object} - */ - feature(name) { - return { - /** - * Checks whether feature is available (permitted by the current license). - * @returns {boolean} - */ - isAvailable: () => { - return !!get(this._cache.response, `features.${name}.available`); - }, - - /** - * Checks whether feature is enabled (not disabled by the configuration specifically). - * @returns {boolean} - */ - isEnabled: () => { - return !!get(this._cache.response, `features.${name}.enabled`); - }, - - /** - * Registers a `generator` function that will be called with XPackInfo instance as - * argument whenever XPack info changes. Whatever `generator` returns will be stored - * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. - * @param {Function} generator Function to call whenever XPackInfo changes. - */ - registerLicenseCheckResultsGenerator: (generator) => { - this._featureLicenseCheckResultsGenerators.set(name, generator); - - // Since JSON representation and signature are cached we should invalidate them to - // include results from newly registered generator when they are requested. - this._cache.json = undefined; - this._cache.signature = undefined; - }, - - /** - * Returns license check results that were previously produced by the `generator` function. - * @returns {Object} - */ - getLicenseCheckResults: () => this.toJSON().features[name] - }; - } - - /** - * Extracts string md5 hash from the stringified version of license JSON representation. - * @returns {string} - */ - getSignature() { - if (this._cache.signature) { - return this._cache.signature; - } - - this._cache.signature = createHash('md5') - .update(JSON.stringify(this.toJSON())) - .digest('hex'); - - return this._cache.signature; - } - - /** - * Returns JSON representation of the license object that is suitable for serialization. - * @returns {Object} - */ - toJSON() { - if (this._cache.json) { - return this._cache.json; - } - - this._cache.json = { - license: { - type: this.license.getType(), - isActive: this.license.isActive(), - expiryDateInMillis: this.license.getExpiryDateInMillis() - }, - features: {} - }; - - // Set response elements specific to each feature. To do this, - // call the license check results generator for each feature, passing them - // the xpack info object - for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { - // return value expected to be a dictionary object. - this._cache.json.features[feature] = licenseChecker(this); - } - - return this._cache.json; - } - - /** - * Checks whether license within specified response differs from the current license. - * Comparison is based on license mode, status and expiration date. - * @param {Object} response xPack info response object returned from the backend. - * @returns {boolean} True if license within specified response object differs from - * the one we already have. - * @private - */ - _hasLicenseInfoChanged(response) { - const newLicense = get(response, 'license') || {}; - const cachedLicense = get(this._cache.response, 'license') || {}; - - if (newLicense.mode !== cachedLicense.mode) { - return true; - } - - if (newLicense.status !== cachedLicense.status) { - return true; - } - - return newLicense.expiry_date_in_millis !== cachedLicense.expiry_date_in_millis; - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts new file mode 100644 index 00000000000000..fbb8929154c36a --- /dev/null +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts @@ -0,0 +1,240 @@ +/* + * 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 { createHash } from 'crypto'; +import { Legacy } from 'kibana'; + +import { XPackInfoLicense } from './xpack_info_license'; + +import { LicensingPluginSetup, ILicense } from '../../../../../plugins/licensing/server'; + +export interface XPackInfoOptions { + clusterSource?: string; + pollFrequencyInMillis: number; +} + +type LicenseGeneratorCheck = (xpackInfo: XPackInfo) => any; + +export interface XPackFeature { + isAvailable(): boolean; + isEnabled(): boolean; + registerLicenseCheckResultsGenerator(generator: LicenseGeneratorCheck): void; + getLicenseCheckResults(): any; +} + +interface Deps { + licensing: LicensingPluginSetup; +} + +/** + * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. + */ +export class XPackInfo { + /** + * XPack License object. + * @type {XPackInfoLicense} + * @private + */ + _license: XPackInfoLicense; + + /** + * Feature name <-> feature license check generator function mapping. + * @type {Map} + * @private + */ + _featureLicenseCheckResultsGenerators = new Map(); + + /** + * Set of listener functions that will be called whenever the license + * info changes + * @type {Set} + */ + _licenseInfoChangedListeners = new Set<() => void>(); + + /** + * Cache that may contain last xpack info API response or error, json representation + * of xpack info and xpack info signature. + * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} + * @private + */ + private _cache: { + license?: ILicense; + error?: string; + json?: Record; + signature?: string; + }; + + /** + * XPack License instance. + * @returns {XPackInfoLicense} + */ + public get license() { + return this._license; + } + + private readonly licensingPlugin: LicensingPluginSetup; + + /** + * Constructs XPack info object. + * @param {Hapi.Server} server HapiJS server instance. + */ + constructor(server: Legacy.Server, deps: Deps) { + if (!deps.licensing) { + throw new Error('XPackInfo requires enabled Licensing plugin'); + } + this.licensingPlugin = deps.licensing; + + this._cache = {}; + + this.licensingPlugin.license$.subscribe((license: ILicense) => { + if (license.isActive) { + this._cache = { + license, + error: undefined, + }; + } else { + this._cache = { + license, + error: license.error, + }; + } + }); + + this._license = new XPackInfoLicense(() => this._cache.license); + } + + /** + * Checks whether XPack info is available. + * @returns {boolean} + */ + isAvailable() { + return Boolean(this._cache.license?.isAvailable); + } + + /** + * Checks whether ES was available + * @returns {boolean} + */ + isXpackUnavailable() { + return ( + this._cache.error && + this._cache.error === 'X-Pack plugin is not installed on the Elasticsearch cluster.' + ); + } + + /** + * If present, describes the reason why XPack info is not available. + * @returns {Error|string} + */ + unavailableReason() { + return this._cache.license?.getUnavailableReason(); + } + + onLicenseInfoChange(handler: () => void) { + this._licenseInfoChangedListeners.add(handler); + } + + /** + * Queries server to get the updated XPack info. + * @returns {Promise.} + */ + async refreshNow() { + await this.licensingPlugin.refresh(); + return this; + } + + /** + * Returns a wrapper around XPack info that gives an access to the properties of + * the specific feature. + * @param {string} name Name of the feature to get a wrapper for. + * @returns {Object} + */ + feature(name: string): XPackFeature { + return { + /** + * Checks whether feature is available (permitted by the current license). + * @returns {boolean} + */ + isAvailable: () => { + return Boolean(this._cache.license?.getFeature(name).isAvailable); + }, + + /** + * Checks whether feature is enabled (not disabled by the configuration specifically). + * @returns {boolean} + */ + isEnabled: () => { + return Boolean(this._cache.license?.getFeature(name).isEnabled); + }, + + /** + * Registers a `generator` function that will be called with XPackInfo instance as + * argument whenever XPack info changes. Whatever `generator` returns will be stored + * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. + * @param {Function} generator Function to call whenever XPackInfo changes. + */ + registerLicenseCheckResultsGenerator: (generator: LicenseGeneratorCheck) => { + this._featureLicenseCheckResultsGenerators.set(name, generator); + + // Since JSON representation and signature are cached we should invalidate them to + // include results from newly registered generator when they are requested. + this._cache.json = undefined; + this._cache.signature = undefined; + }, + + /** + * Returns license check results that were previously produced by the `generator` function. + * @returns {Object} + */ + getLicenseCheckResults: () => this.toJSON().features[name], + }; + } + + /** + * Extracts string md5 hash from the stringified version of license JSON representation. + * @returns {string} + */ + getSignature() { + if (this._cache.signature) { + return this._cache.signature; + } + + this._cache.signature = createHash('md5') + .update(JSON.stringify(this.toJSON())) + .digest('hex'); + + return this._cache.signature; + } + + /** + * Returns JSON representation of the license object that is suitable for serialization. + * @returns {Object} + */ + toJSON() { + if (this._cache.json) { + return this._cache.json; + } + + this._cache.json = { + license: { + type: this.license.getType(), + isActive: this.license.isActive(), + expiryDateInMillis: this.license.getExpiryDateInMillis(), + }, + features: {}, + }; + + // Set response elements specific to each feature. To do this, + // call the license check results generator for each feature, passing them + // the xpack info object + for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { + // return value expected to be a dictionary object. + this._cache.json.features[feature] = licenseChecker(this); + } + + return this._cache.json; + } +} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts deleted file mode 100644 index ab09e0d73b80da..00000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -type LicenseType = 'oss' | 'basic' | 'trial' | 'standard' | 'basic' | 'gold' | 'platinum'; - -export declare class XPackInfoLicense { - constructor(getRawLicense: () => any); - - public getUid(): string | undefined; - public isActive(): boolean; - public getExpiryDateInMillis(): number | undefined; - public isOneOf(candidateLicenses: string[]): boolean; - public getType(): LicenseType | undefined; - public getMode(): string | undefined; - public isActiveLicense(typeChecker: (mode: string) => boolean): boolean; - public isBasic(): boolean; - public isNotBasic(): boolean; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js index 300110744e9793..9cf1e141e0981f 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { licensingMock } from '../../../../../plugins/licensing/server/licensing.mock'; import { XPackInfoLicense } from './xpack_info_license'; function getXPackInfoLicense(getRawLicense) { @@ -24,7 +25,7 @@ describe('XPackInfoLicense', () => { test('getUid returns uid field', () => { const uid = 'abc123'; - getRawLicense.mockReturnValue({ uid }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { uid } })); expect(xpackInfoLicense.getUid()).toBe(uid); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -33,14 +34,14 @@ describe('XPackInfoLicense', () => { }); test('isActive returns true if status is active', () => { - getRawLicense.mockReturnValue({ status: 'active' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active' } })); expect(xpackInfoLicense.isActive()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); }); test('isActive returns false if status is not active', () => { - getRawLicense.mockReturnValue({ status: 'aCtIvE' }); // needs to match exactly + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'aCtIvE' } })); // needs to match exactly expect(xpackInfoLicense.isActive()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -49,7 +50,7 @@ describe('XPackInfoLicense', () => { }); test('getExpiryDateInMillis returns expiry_date_in_millis', () => { - getRawLicense.mockReturnValue({ expiry_date_in_millis: 123 }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { expiryDateInMillis: 123 } })); expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -58,7 +59,7 @@ describe('XPackInfoLicense', () => { }); test('isOneOf returns true of the mode includes one of the types', () => { - getRawLicense.mockReturnValue({ mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'platinum' } })); expect(xpackInfoLicense.isOneOf('platinum')).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); @@ -78,12 +79,12 @@ describe('XPackInfoLicense', () => { }); test('getType returns the type', () => { - getRawLicense.mockReturnValue({ type: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'basic' } })); expect(xpackInfoLicense.getType()).toBe('basic'); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ type: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'gold' } })); expect(xpackInfoLicense.getType()).toBe('gold'); expect(getRawLicense).toHaveBeenCalledTimes(2); @@ -92,12 +93,12 @@ describe('XPackInfoLicense', () => { }); test('getMode returns the mode', () => { - getRawLicense.mockReturnValue({ mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'basic' } })); expect(xpackInfoLicense.getMode()).toBe('basic'); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'gold' } })); expect(xpackInfoLicense.getMode()).toBe('gold'); expect(getRawLicense).toHaveBeenCalledTimes(2); @@ -108,22 +109,22 @@ describe('XPackInfoLicense', () => { test('isActiveLicense returns the true if active and typeChecker matches', () => { const expectAbc123 = type => type === 'abc123'; - getRawLicense.mockReturnValue({ status: 'active', mode: 'abc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'abc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'abc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'abc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'NOTabc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'NOTabc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'NOTabc123' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'NOTabc123' } })); expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); @@ -132,22 +133,22 @@ describe('XPackInfoLicense', () => { }); test('isBasic returns the true if active and basic', () => { - getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })); expect(xpackInfoLicense.isBasic()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })); expect(xpackInfoLicense.isBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); @@ -157,22 +158,22 @@ describe('XPackInfoLicense', () => { test('isNotBasic returns the true if active and not basic', () => { - getRawLicense.mockReturnValue({ status: 'active', mode: 'platinum' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } })); expect(xpackInfoLicense.isNotBasic()).toBe(true); expect(getRawLicense).toHaveBeenCalledTimes(1); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'gold' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(2); - getRawLicense.mockReturnValue({ status: 'NOTactive', mode: 'trial' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(3); - getRawLicense.mockReturnValue({ status: 'active', mode: 'basic' }); + getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } })); expect(xpackInfoLicense.isNotBasic()).toBe(false); expect(getRawLicense).toHaveBeenCalledTimes(4); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts similarity index 74% rename from x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js rename to x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts index b87bae9e403dde..e1951a4bca047a 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; +import { ILicense } from '../../../../../plugins/licensing/server'; /** * "View" for XPack Info license information. @@ -15,9 +15,9 @@ export class XPackInfoLicense { * @type {Function} * @private */ - _getRawLicense = null; + _getRawLicense: () => ILicense | undefined; - constructor(getRawLicense) { + constructor(getRawLicense: () => ILicense | undefined) { this._getRawLicense = getRawLicense; } @@ -26,7 +26,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getUid() { - return get(this._getRawLicense(), 'uid'); + return this._getRawLicense()?.uid; } /** @@ -34,7 +34,7 @@ export class XPackInfoLicense { * @returns {boolean} */ isActive() { - return get(this._getRawLicense(), 'status') === 'active'; + return Boolean(this._getRawLicense()?.isActive); } /** @@ -45,7 +45,7 @@ export class XPackInfoLicense { * @returns {number|undefined} */ getExpiryDateInMillis() { - return get(this._getRawLicense(), 'expiry_date_in_millis'); + return this._getRawLicense()?.expiryDateInMillis; } /** @@ -53,12 +53,10 @@ export class XPackInfoLicense { * @param {String} candidateLicenses List of the licenses to check against. * @returns {boolean} */ - isOneOf(candidateLicenses) { - if (!Array.isArray(candidateLicenses)) { - candidateLicenses = [candidateLicenses]; - } - - return candidateLicenses.includes(get(this._getRawLicense(), 'mode')); + isOneOf(candidateLicenses: string | string[]) { + const candidates = Array.isArray(candidateLicenses) ? candidateLicenses : [candidateLicenses]; + const mode = this._getRawLicense()?.mode; + return Boolean(mode && candidates.includes(mode)); } /** @@ -66,7 +64,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getType() { - return get(this._getRawLicense(), 'type'); + return this._getRawLicense()?.type; } /** @@ -74,7 +72,7 @@ export class XPackInfoLicense { * @returns {string|undefined} */ getMode() { - return get(this._getRawLicense(), 'mode'); + return this._getRawLicense()?.mode; } /** @@ -83,10 +81,10 @@ export class XPackInfoLicense { * @param {Function} typeChecker The license type checker. * @returns {boolean} */ - isActiveLicense(typeChecker) { + isActiveLicense(typeChecker: (mode: string) => boolean) { const license = this._getRawLicense(); - return get(license, 'status') === 'active' && typeChecker(get(license, 'mode')); + return Boolean(license?.isActive && typeChecker(license.mode as any)); } /** diff --git a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts similarity index 78% rename from x-pack/legacy/plugins/xpack_main/xpack_main.d.ts rename to x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index 2a197811cc0329..05cb97663e1af6 100644 --- a/x-pack/legacy/plugins/xpack_main/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -5,9 +5,9 @@ */ import KbnServer from 'src/legacy/server/kbn_server'; -import { Feature, FeatureWithAllOrReadPrivileges } from '../../../plugins/features/server'; -import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info'; -export { XPackFeature } from './server/lib/xpack_info'; +import { Feature, FeatureWithAllOrReadPrivileges } from '../../../../plugins/features/server'; +import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; +export { XPackFeature } from './lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; diff --git a/x-pack/legacy/server/lib/esjs_shield_plugin.js b/x-pack/legacy/server/lib/esjs_shield_plugin.js deleted file mode 100644 index b6252035aa321f..00000000000000 --- a/x-pack/legacy/server/lib/esjs_shield_plugin.js +++ /dev/null @@ -1,579 +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. - */ - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { // eslint-disable-line no-undef - define([], factory); // eslint-disable-line no-undef - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.ElasticsearchShield = factory(); - } -}(this, function () { - return function addShieldApi(Client, config, components) { - const ca = components.clientAction.factory; - - Client.prototype.shield = components.clientAction.namespaceFactory(); - const shield = Client.prototype.shield.prototype; - - /** - * Perform a [shield.authenticate](Retrieve details about the currently authenticated user) request - * - * @param {Object} params - An object with parameters used to carry out this action - */ - shield.authenticate = ca({ - params: {}, - url: { - fmt: '/_security/_authenticate' - } - }); - - /** - * Perform a [shield.changePassword](Change the password of a user) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the user to change the password for - */ - shield.changePassword = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - urls: [ - { - fmt: '/_security/user/<%=username%>/_password', - req: { - username: { - type: 'string', - required: false - } - } - }, - { - fmt: '/_security/user/_password' - } - ], - needBody: true, - method: 'POST' - }); - - /** - * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache - * @param {String} params.realms - Comma-separated list of realms to clear - */ - shield.clearCachedRealms = ca({ - params: { - usernames: { - type: 'string', - required: false - } - }, - url: { - fmt: '/_security/realm/<%=realms%>/_clear_cache', - req: { - realms: { - type: 'string', - required: true - } - } - }, - method: 'POST' - }); - - /** - * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.clearCachedRoles = ca({ - params: {}, - url: { - fmt: '/_security/role/<%=name%>/_clear_cache', - req: { - name: { - type: 'string', - required: true - } - } - }, - method: 'POST' - }); - - /** - * Perform a [shield.deleteRole](Remove a role from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.deleteRole = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true - } - } - }, - method: 'DELETE' - }); - - /** - * Perform a [shield.deleteUser](Remove a user from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - username - */ - shield.deleteUser = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true - } - } - }, - method: 'DELETE' - }); - - /** - * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String} params.name - Role name - */ - shield.getRole = ca({ - params: {}, - urls: [ - { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: false - } - } - }, - { - fmt: '/_security/role' - } - ] - }); - - /** - * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {String, String[], Boolean} params.username - A comma-separated list of usernames - */ - shield.getUser = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'list', - required: false - } - } - }, - { - fmt: '/_security/user' - } - ] - }); - - /** - * Perform a [shield.putRole](Update or create a role for the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.name - Role name - */ - shield.putRole = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/role/<%=name%>', - req: { - name: { - type: 'string', - required: true - } - } - }, - needBody: true, - method: 'PUT' - }); - - /** - * Perform a [shield.putUser](Update or create a user for the native shield realm) request - * - * @param {Object} params - An object with parameters used to carry out this action - * @param {Boolean} params.refresh - Refresh the index after performing the operation - * @param {String} params.username - The username of the User - */ - shield.putUser = ca({ - params: { - refresh: { - type: 'boolean' - } - }, - url: { - fmt: '/_security/user/<%=username%>', - req: { - username: { - type: 'string', - required: true - } - } - }, - needBody: true, - method: 'PUT' - }); - - /** - * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request - * - */ - shield.getUserPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/user/_privileges' - } - ] - }); - - /** - * Asks Elasticsearch to prepare SAML authentication request to be sent to - * the 3rd-party SAML identity provider. - * - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL - * in the Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm. - * - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{realm: string, id: string, redirect: string}} Object that includes identifier - * of the SAML realm used to prepare authentication request, encrypted request token to be - * sent to Elasticsearch with SAML response and redirect URL to the identity provider that - * will be used to authenticate user. - */ - shield.samlPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/prepare' - } - }); - - /** - * Sends SAML response returned by identity provider to Elasticsearch for validation. - * - * @param {Array.} ids A list of encrypted request tokens returned within SAML - * preparation response. - * @param {string} content SAML response returned by identity provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.samlAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/authenticate' - } - }); - - /** - * Invalidates SAML access token. - * - * @param {string} token SAML access token that needs to be invalidated. - * - * @returns {{redirect?: string}} - */ - shield.samlLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/logout' - } - }); - - /** - * Invalidates SAML session based on Logout Request received from the Identity Provider. - * - * @param {string} queryString URL encoded query string provided by Identity Provider. - * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL in the - * Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch - * will choose the right SAML realm to invalidate session. - * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. - * - * @returns {{redirect?: string}} - */ - shield.samlInvalidate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/saml/invalidate' - } - }); - - /** - * Asks Elasticsearch to prepare an OpenID Connect authentication request to be sent to - * the 3rd-party OpenID Connect provider. - * - * @param {string} realm The OpenID Connect realm name in Elasticsearch - * - * @returns {{state: string, nonce: string, redirect: string}} Object that includes two opaque parameters that need - * to be sent to Elasticsearch with the OpenID Connect response and redirect URL to the OpenID Connect provider that - * will be used to authenticate user. - */ - shield.oidcPrepare = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/prepare' - } - }); - - /** - * Sends the URL to which the OpenID Connect Provider redirected the UA to Elasticsearch for validation. - * - * @param {string} state The state parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} nonce The nonce parameter that was returned by Elasticsearch in the - * preparation response. - * @param {string} redirect_uri The URL to where the UA was redirected by the OpenID Connect provider. - * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm - * that should be used to authenticate request. - * - * @returns {{username: string, access_token: string, refresh_token; string, expires_in: number}} Object that - * includes name of the user, access token to use for any consequent requests that - * need to be authenticated and a number of seconds after which access token will expire. - */ - shield.oidcAuthenticate = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/authenticate' - } - }); - - /** - * Invalidates an access token and refresh token pair that was generated after an OpenID Connect authentication. - * - * @param {string} token An access token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * @param {string} refresh_token A refresh token that was created by authenticating to an OpenID Connect realm and - * that needs to be invalidated. - * - * @returns {{redirect?: string}} If the Elasticsearch OpenID Connect realm configuration and the - * OpenID Connect provider supports RP-initiated SLO, a URL to redirect the UA - */ - shield.oidcLogout = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oidc/logout' - } - }); - - /** - * Refreshes an access token. - * - * @param {string} grant_type Currently only "refresh_token" grant type is supported. - * @param {string} refresh_token One-time refresh token that will be exchanged to the new access/refresh token pair. - * - * @returns {{access_token: string, type: string, expires_in: number, refresh_token: string}} - */ - shield.getAccessToken = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/oauth2/token' - } - }); - - /** - * Invalidates an access token. - * - * @param {string} token The access token to invalidate - * - * @returns {{created: boolean}} - */ - shield.deleteAccessToken = ca({ - method: 'DELETE', - needBody: true, - params: { - token: { - type: 'string' - } - }, - url: { - fmt: '/_security/oauth2/token' - } - }); - - shield.getPrivilege = ca({ - method: 'GET', - urls: [{ - fmt: '/_security/privilege/<%=privilege%>', - req: { - privilege: { - type: 'string', - required: false - } - } - }, { - fmt: '/_security/privilege' - }] - }); - - shield.deletePrivilege = ca({ - method: 'DELETE', - urls: [{ - fmt: '/_security/privilege/<%=application%>/<%=privilege%>', - req: { - application: { - type: 'string', - required: true - }, - privilege: { - type: 'string', - required: true - } - } - }] - }); - - shield.postPrivileges = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/privilege' - } - }); - - shield.hasPrivileges = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/user/_has_privileges' - } - }); - - shield.getBuiltinPrivileges = ca({ - params: {}, - urls: [ - { - fmt: '/_security/privilege/_builtin' - } - ] - }); - - /** - * Gets API keys in Elasticsearch - * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. - * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as - * they are assumed to be the currently authenticated ones. - */ - shield.getAPIKeys = ca({ - method: 'GET', - urls: [{ - fmt: `/_security/api_key?owner=<%=owner%>`, - req: { - owner: { - type: 'boolean', - required: true - } - } - }] - }); - - /** - * Creates an API key in Elasticsearch for the current user. - * - * @param {string} name A name for this API key - * @param {object} role_descriptors Role descriptors for this API key, if not - * provided then permissions of authenticated user are applied. - * @param {string} [expiration] Optional expiration for the API key being generated. If expiration - * is not provided then the API keys do not expire. - * - * @returns {{id: string, name: string, api_key: string, expiration?: number}} - */ - shield.createAPIKey = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/api_key', - }, - }); - - /** - * Invalidates an API key in Elasticsearch. - * - * @param {string} [id] An API key id. - * @param {string} [name] An API key name. - * @param {string} [realm_name] The name of an authentication realm. - * @param {string} [username] The username of a user. - * - * NOTE: While all parameters are optional, at least one of them is required. - * - * @returns {{invalidated_api_keys: string[], previously_invalidated_api_keys: string[], error_count: number, error_details?: object[]}} - */ - shield.invalidateAPIKey = ca({ - method: 'DELETE', - needBody: true, - url: { - fmt: '/_security/api_key', - }, - }); - - /** - * Gets an access token in exchange to the certificate chain for the target subject distinguished name. - * - * @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not - * base64url-encoded) DER PKIX certificate values. - * - * @returns {{access_token: string, type: string, expires_in: number}} - */ - shield.delegatePKI = ca({ - method: 'POST', - needBody: true, - url: { - fmt: '/_security/delegate_pki', - }, - }); - }; -})); diff --git a/x-pack/legacy/server/lib/get_client_shield.ts b/x-pack/legacy/server/lib/get_client_shield.ts deleted file mode 100644 index 1f68c2e6d3466f..00000000000000 --- a/x-pack/legacy/server/lib/get_client_shield.ts +++ /dev/null @@ -1,14 +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 { once } from 'lodash'; -import { Legacy } from 'kibana'; -// @ts-ignore -import esShield from './esjs_shield_plugin'; - -export const getClient = once((server: Legacy.Server) => { - return server.plugins.elasticsearch.createCluster('security', { plugins: [esShield] }); -}); diff --git a/x-pack/package.json b/x-pack/package.json index a92839eadefa41..b1fc430adbbeea 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -91,14 +91,14 @@ "@types/react": "^16.9.11", "@types/react-dom": "^16.9.4", "@types/react-redux": "^6.0.6", - "@types/react-router-dom": "^4.3.1", + "@types/react-router-dom": "^5.1.3", "@types/react-sticky": "^6.0.3", "@types/react-test-renderer": "^16.9.1", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^0.3.0", "@types/redux-actions": "^2.2.1", "@types/sinon": "^7.0.13", - "@types/styled-components": "^4.4.0", + "@types/styled-components": "^4.4.1", "@types/supertest": "^2.0.5", "@types/tar-fs": "^1.16.1", "@types/tinycolor2": "^1.4.1", @@ -114,7 +114,7 @@ "chalk": "^2.4.2", "chance": "1.0.18", "cheerio": "0.22.0", - "commander": "3.0.0", + "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", "cypress": "^3.6.1", "cypress-multi-reporters": "^1.2.3", @@ -302,7 +302,7 @@ "react-portal": "^3.2.0", "react-redux": "^5.1.2", "react-reverse-portal": "^1.0.4", - "react-router-dom": "^4.3.1", + "react-router-dom": "^5.1.2", "react-shortcuts": "^2.0.0", "react-sticky": "^6.0.3", "react-syntax-highlighter": "^5.7.0", @@ -345,7 +345,7 @@ "xregexp": "4.2.4" }, "engines": { - "yarn": "^1.10.1" + "yarn": "^1.21.1" }, "workspaces": { "nohoist": [ diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 53eb4a909cebee..b0e10d245e0b95 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -48,4 +48,4 @@ export type APMConfig = ReturnType; export const plugin = (initContext: PluginInitializerContext) => new APMPlugin(initContext); -export { APMPlugin } from './plugin'; +export { APMPlugin, APMPluginContract } from './plugin'; diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts new file mode 100644 index 00000000000000..d3a69c01732fa6 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeCreateCustomElementRoute } from './create'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const mockedUUID = '123abc'; +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +describe('POST custom element', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + + const router = httpService.createRouter('') as jest.Mocked; + initializeCreateCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + afterEach(() => { + clock.restore(); + }); + + it(`returns 200 when the custom element is created`, async () => { + const mockCustomElement = { + displayName: 'My Custom Element', + }; + + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/custom-element', + body: mockCustomElement, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + { + ...mockCustomElement, + '@timestamp': nowIso, + '@created': nowIso, + }, + { + id: `custom-element-${mockedUUID}`, + } + ); + }); + + it(`returns bad request if create is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'post', + path: 'api/canvas/custom-element', + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/create.ts b/x-pack/plugins/canvas/server/routes/custom_elements/create.ts new file mode 100644 index 00000000000000..b8828291246966 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/create.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { CustomElementSchema } from './custom_element_schema'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeCreateCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.post( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}`, + validate: { + body: CustomElementSchema, + }, + options: { + body: { + maxBytes: 26214400, // 25MB payload limit + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const customElement = request.body; + + const now = new Date().toISOString(); + const { id, ...payload } = customElement; + + await context.core.savedObjects.client.create( + CUSTOM_ELEMENT_TYPE, + { + ...payload, + '@timestamp': now, + '@created': now, + }, + { id: id || getId('custom-element') } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.ts new file mode 100644 index 00000000000000..e76526eeeb27b9 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_attributes.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 { CustomElement } from '../../../../../legacy/plugins/canvas/types'; + +// Exclude ID attribute for the type used for SavedObjectClient +export type CustomElementAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.ts new file mode 100644 index 00000000000000..956dccc5aaea21 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/custom_element_schema.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 { schema } from '@kbn/config-schema'; + +export const CustomElementSchema = schema.object({ + '@created': schema.maybe(schema.string()), + '@timestamp': schema.maybe(schema.string()), + content: schema.string(), + displayName: schema.string(), + help: schema.maybe(schema.string()), + id: schema.string(), + image: schema.maybe(schema.string()), + name: schema.string(), + tags: schema.maybe(schema.arrayOf(schema.string())), +}); + +export const CustomElementUpdateSchema = schema.object({ + displayName: schema.string(), + help: schema.maybe(schema.string()), + image: schema.maybe(schema.string()), + name: schema.string(), +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts new file mode 100644 index 00000000000000..c108f2316db272 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeDeleteCustomElementRoute } from './delete'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('DELETE custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeDeleteCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it(`returns 200 ok when the custom element is deleted`, async () => { + const id = 'some-id'; + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/custom-element/${id}`, + params: { + id, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual({ ok: true }); + expect(mockRouteContext.core.savedObjects.client.delete).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + id + ); + }); + + it(`returns bad request if delete is unsuccessful`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'delete', + path: `api/canvas/custom-element/some-id`, + params: { + id: 'some-id', + }, + }); + + (mockRouteContext.core.savedObjects.client.delete as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts b/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts new file mode 100644 index 00000000000000..5867539b95b532 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeDeleteCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.delete( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + await context.core.savedObjects.client.delete(CUSTOM_ELEMENT_TYPE, request.params.id); + return response.ok({ body: okResponse }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts new file mode 100644 index 00000000000000..6644d3b56c6815 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { initializeFindCustomElementsRoute } from './find'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('Find custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeFindCustomElementsRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 with the found custom elements`, async () => { + const name = 'something'; + const perPage = 10000; + const mockResults = { + total: 2, + saved_objects: [ + { id: 1, attributes: { key: 'value' } }, + { id: 2, attributes: { key: 'other-value' } }, + ], + }; + + const findMock = mockRouteContext.core.savedObjects.client.find as jest.Mock; + + findMock.mockResolvedValueOnce(mockResults); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/custom-elements/find`, + query: { + name, + perPage, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(200); + expect(findMock.mock.calls[0][0].search).toBe(`${name}* | ${name}`); + expect(findMock.mock.calls[0][0].perPage).toBe(perPage); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "customElements": Array [ + Object { + "id": 1, + "key": "value", + }, + Object { + "id": 2, + "key": "other-value", + }, + ], + "total": 2, + } + `); + }); + + it(`returns 200 with empty results on error`, async () => { + (mockRouteContext.core.savedObjects.client.find as jest.Mock).mockImplementationOnce(() => { + throw new Error('generic error'); + }); + + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: `api/canvas/custom-elements/find`, + query: { + name: 'something', + perPage: 1000, + }, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "customElements": Array [], + "total": 0, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/find.ts b/x-pack/plugins/canvas/server/routes/custom_elements/find.ts new file mode 100644 index 00000000000000..5041ceb3e4711d --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/find.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { SavedObjectAttributes } from 'src/core/server'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; + +export function initializeFindCustomElementsRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/find`, + validate: { + query: schema.object({ + name: schema.string(), + page: schema.maybe(schema.number()), + perPage: schema.number(), + }), + }, + }, + async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const { name, page, perPage } = request.query; + + try { + const customElements = await savedObjectsClient.find({ + type: CUSTOM_ELEMENT_TYPE, + sortField: '@timestamp', + sortOrder: 'desc', + search: name ? `${name}* | ${name}` : '*', + searchFields: ['name'], + fields: [ + 'id', + 'name', + 'displayName', + 'help', + 'image', + 'content', + '@created', + '@timestamp', + ], + page, + perPage, + }); + + return response.ok({ + body: { + total: customElements.total, + customElements: customElements.saved_objects.map(hit => ({ + id: hit.id, + ...hit.attributes, + })), + }, + }); + } catch (error) { + return response.ok({ + body: { + total: 0, + customElements: [], + }, + }); + } + } + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts new file mode 100644 index 00000000000000..5e8d536f779a9a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeGetCustomElementRoute } from './get'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +describe('GET custom element', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeGetCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it(`returns 200 when the custom element is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/custom-element/123', + params: { + id: '123', + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: CUSTOM_ELEMENT_TYPE, + attributes: { foo: true }, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "foo": true, + "id": "123", + } + `); + + expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "canvas-element", + "123", + ], + ] + `); + }); + + it('returns 404 if the custom element is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/custom-element/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CUSTOM_ELEMENT_TYPE, id); + }); + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-element/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/get.ts b/x-pack/plugins/canvas/server/routes/custom_elements/get.ts new file mode 100644 index 00000000000000..f092b001e141f7 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/get.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeGetCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const customElement = await context.core.savedObjects.client.get( + CUSTOM_ELEMENT_TYPE, + request.params.id + ); + + return response.ok({ + body: { + id: customElement.id, + ...customElement.attributes, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/index.ts b/x-pack/plugins/canvas/server/routes/custom_elements/index.ts new file mode 100644 index 00000000000000..ade641e4913711 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { RouteInitializerDeps } from '../'; +import { initializeFindCustomElementsRoute } from './find'; +import { initializeGetCustomElementRoute } from './get'; +import { initializeCreateCustomElementRoute } from './create'; +import { initializeUpdateCustomElementRoute } from './update'; +import { initializeDeleteCustomElementRoute } from './delete'; + +export function initCustomElementsRoutes(deps: RouteInitializerDeps) { + initializeFindCustomElementsRoute(deps); + initializeGetCustomElementRoute(deps); + initializeCreateCustomElementRoute(deps); + initializeUpdateCustomElementRoute(deps); + initializeDeleteCustomElementRoute(deps); +} diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts new file mode 100644 index 00000000000000..f21a9c25b6e64a --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import sinon from 'sinon'; +import { CustomElement } from '../../../../../legacy/plugins/canvas/types'; +import { CUSTOM_ELEMENT_TYPE } from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { initializeUpdateCustomElementRoute } from './update'; +import { + IRouter, + kibanaResponseFactory, + RequestHandlerContext, + RequestHandler, +} from 'src/core/server'; +import { + savedObjectsClientMock, + httpServiceMock, + httpServerMock, + loggingServiceMock, +} from 'src/core/server/mocks'; +import { okResponse } from '../ok_response'; + +const mockRouteContext = ({ + core: { + savedObjects: { + client: savedObjectsClientMock.create(), + }, + }, +} as unknown) as RequestHandlerContext; + +const now = new Date(); +const nowIso = now.toISOString(); + +jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); + +type CustomElementPayload = CustomElement & { + '@timestamp': string; + '@created': string; +}; + +const customElement: CustomElementPayload = { + id: 'my-custom-element', + name: 'MyCustomElement', + displayName: 'My Wonderful Custom Element', + content: 'This is content', + tags: ['filter', 'graphic'], + '@created': '2019-02-08T18:35:23.029Z', + '@timestamp': '2019-02-08T18:35:23.029Z', +}; + +describe('PUT custom element', () => { + let routeHandler: RequestHandler; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now); + + const httpService = httpServiceMock.createSetupContract(); + const router = httpService.createRouter('') as jest.Mocked; + initializeUpdateCustomElementRoute({ + router, + logger: loggingServiceMock.create().get(), + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + clock.restore(); + }); + + it(`returns 200 ok when the custom element is updated`, async () => { + const updatedCustomElement = { name: 'new name' }; + const { id, ...customElementAttributes } = customElement; + + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: `api/canvas/custom-element/${id}`, + params: { + id, + }, + body: updatedCustomElement, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id, + type: CUSTOM_ELEMENT_TYPE, + attributes: customElementAttributes as any, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(okResponse); + expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( + CUSTOM_ELEMENT_TYPE, + { + ...customElementAttributes, + ...updatedCustomElement, + '@timestamp': nowIso, + '@created': customElement['@created'], + }, + { + overwrite: true, + id, + } + ); + }); + + it(`returns not found if existing custom element is not found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/custom-element/some-id', + params: { + id: 'not-found', + }, + body: {}, + }); + + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( + 'not found' + ); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + expect(response.status).toBe(404); + }); + + it(`returns bad request if the write fails`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'put', + path: 'api/canvas/custom-element/some-id', + params: { + id: 'some-id', + }, + body: {}, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'some-id', + type: CUSTOM_ELEMENT_TYPE, + attributes: {}, + references: [], + }); + + mockRouteContext.core.savedObjects.client = savedObjectsClient; + (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(400); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts new file mode 100644 index 00000000000000..51c363249dd793 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { omit } from 'lodash'; +import { RouteInitializerDeps } from '../'; +import { + CUSTOM_ELEMENT_TYPE, + API_ROUTE_CUSTOM_ELEMENT, +} from '../../../../../legacy/plugins/canvas/common/lib/constants'; +import { CustomElementUpdateSchema } from './custom_element_schema'; +import { CustomElementAttributes } from './custom_element_attributes'; +import { okResponse } from '../ok_response'; +import { catchErrorHandler } from '../catch_error_handler'; + +export function initializeUpdateCustomElementRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.put( + { + path: `${API_ROUTE_CUSTOM_ELEMENT}/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: CustomElementUpdateSchema, + }, + options: { + body: { + maxBytes: 26214400, // 25MB payload limit + accepts: ['application/json'], + }, + }, + }, + catchErrorHandler(async (context, request, response) => { + const payload = request.body; + const id = request.params.id; + + const now = new Date().toISOString(); + + const customElementObject = await context.core.savedObjects.client.get< + CustomElementAttributes + >(CUSTOM_ELEMENT_TYPE, id); + + await context.core.savedObjects.client.create( + CUSTOM_ELEMENT_TYPE, + { + ...customElementObject.attributes, + ...omit(payload, 'id'), // never write the id property + '@timestamp': now, + '@created': customElementObject.attributes['@created'], // ensure created is not modified + }, + { overwrite: true, id } + ); + + return response.ok({ + body: okResponse, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index 46873a6b325423..8b2d77d6347609 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -6,6 +6,7 @@ import { IRouter, Logger } from 'src/core/server'; import { initWorkpadRoutes } from './workpad'; +import { initCustomElementsRoutes } from './custom_elements'; export interface RouteInitializerDeps { router: IRouter; @@ -14,4 +15,5 @@ export interface RouteInitializerDeps { export function initRoutes(deps: RouteInitializerDeps) { initWorkpadRoutes(deps); + initCustomElementsRoutes(deps); } diff --git a/x-pack/plugins/canvas/server/routes/workpad/ok_response.ts b/x-pack/plugins/canvas/server/routes/ok_response.ts similarity index 100% rename from x-pack/plugins/canvas/server/routes/workpad/ok_response.ts rename to x-pack/plugins/canvas/server/routes/ok_response.ts diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts index be904356720b68..fc847d4816dbd4 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -11,15 +11,11 @@ import { } from '../../../../../legacy/plugins/canvas/common/lib/constants'; import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; import { getId } from '../../../../../legacy/plugins/canvas/public/lib/get_id'; +import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema } from './workpad_schema'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; router.post( diff --git a/x-pack/plugins/canvas/server/routes/workpad/delete.ts b/x-pack/plugins/canvas/server/routes/workpad/delete.ts index 7adf11e7a887be..8de4ea0f9a27f3 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/delete.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/delete.ts @@ -10,7 +10,7 @@ import { CANVAS_TYPE, API_ROUTE_WORKPAD, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; export function initializeDeleteWorkpadRoute(deps: RouteInitializerDeps) { diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts index 7a51006aa9f024..d7a5e77670f6e5 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -10,14 +10,9 @@ import { CANVAS_TYPE, API_ROUTE_WORKPAD, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadAttributes } from './workpad_attributes'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; router.get( diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts index 492a6c98d71ee3..de098dd9717ed0 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -20,7 +20,7 @@ import { loggingServiceMock, } from 'src/core/server/mocks'; import { workpads } from '../../../../../legacy/plugins/canvas/__tests__/fixtures/workpads'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; const mockRouteContext = ({ core: { diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index 460aa174038ae8..74dedb605472c9 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -15,16 +15,11 @@ import { API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, } from '../../../../../legacy/plugins/canvas/common/lib/constants'; -import { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; +import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema, WorkpadAssetSchema } from './workpad_schema'; -import { okResponse } from './ok_response'; +import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; -export type WorkpadAttributes = Pick> & { - '@timestamp': string; - '@created': string; -}; - const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); const AssetPayloadSchema = schema.object({ diff --git a/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts b/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.ts new file mode 100644 index 00000000000000..2b7b6cca4ba2bc --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/workpad_attributes.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 { CanvasWorkpad } from '../../../../../legacy/plugins/canvas/types'; + +export type WorkpadAttributes = Pick> & { + '@timestamp': string; + '@created': string; +}; diff --git a/x-pack/plugins/features/server/feature.ts b/x-pack/plugins/features/common/feature.ts similarity index 100% rename from x-pack/plugins/features/server/feature.ts rename to x-pack/plugins/features/common/feature.ts diff --git a/x-pack/plugins/features/server/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts similarity index 100% rename from x-pack/plugins/features/server/feature_kibana_privileges.ts rename to x-pack/plugins/features/common/feature_kibana_privileges.ts diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts new file mode 100644 index 00000000000000..6111d7d25a61b9 --- /dev/null +++ b/x-pack/plugins/features/common/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { FeatureKibanaPrivileges } from './feature_kibana_privileges'; +export * from './feature'; diff --git a/x-pack/plugins/features/public/index.ts b/x-pack/plugins/features/public/index.ts new file mode 100644 index 00000000000000..6a2c99aad4bd8e --- /dev/null +++ b/x-pack/plugins/features/public/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 { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common'; diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 66460a811009eb..7b250358926689 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -5,7 +5,7 @@ */ import { FeatureRegistry } from './feature_registry'; -import { Feature } from './feature'; +import { Feature } from '../common/feature'; describe('FeatureRegistry', () => { it('allows a minimal feature to be registered', () => { diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index bec0ab1ed0bf7d..60a229fc58612c 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -5,8 +5,7 @@ */ import { cloneDeep, uniq } from 'lodash'; -import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; -import { Feature, FeatureWithAllOrReadPrivileges } from './feature'; +import { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common'; import { validateFeature } from './feature_schema'; export class FeatureRegistry { diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index b5ba10f8d0300b..8926bd766be32e 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -8,7 +8,7 @@ import Joi from 'joi'; import { difference } from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { FeatureWithAllOrReadPrivileges } from './feature'; +import { FeatureWithAllOrReadPrivileges } from '../common/feature'; // Each feature gets its own property on the UICapabilities object, // but that object has a few built-in properties which should not be overwritten. diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 6a08c25fb7780b..2b4f85aa04f046 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -13,8 +13,7 @@ import { Plugin } from './plugin'; // run-time contracts. export { uiCapabilitiesRegex } from './feature_schema'; -export { Feature, FeatureWithAllOrReadPrivileges } from './feature'; -export { FeatureKibanaPrivileges } from './feature_kibana_privileges'; +export { Feature, FeatureWithAllOrReadPrivileges, FeatureKibanaPrivileges } from '../common'; export { PluginSetupContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index d70f72a7ff0854..b48963ebb81396 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { Feature } from './feature'; +import { Feature } from '../common/feature'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index f3a3375d1936ba..96a8e68f8326d5 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -15,7 +15,7 @@ import { deepFreeze } from '../../../../src/core/utils'; import { XPackInfo } from '../../../legacy/plugins/xpack_main/server/lib/xpack_info'; import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/timelion/server'; import { FeatureRegistry } from './feature_registry'; -import { Feature, FeatureWithAllOrReadPrivileges } from './feature'; +import { Feature, FeatureWithAllOrReadPrivileges } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; import { defineRoutes } from './routes'; diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index 368b38ce7df918..a13afa854de52d 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { Feature } from './feature'; +import { Feature } from '../common/feature'; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; diff --git a/x-pack/plugins/graph/common/check_license.ts b/x-pack/plugins/graph/common/check_license.ts new file mode 100644 index 00000000000000..a918f53776b174 --- /dev/null +++ b/x-pack/plugins/graph/common/check_license.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ILicense, LICENSE_CHECK_STATE } from '../../licensing/common/types'; +import { assertNever } from '../../../../src/core/utils'; + +export interface GraphLicenseInformation { + showAppLink: boolean; + enableAppLink: boolean; + message: string; +} + +export function checkLicense(license: ILicense | undefined): GraphLicenseInformation { + if (!license || !license.isAvailable) { + return { + showAppLink: true, + enableAppLink: false, + message: i18n.translate( + 'xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage', + { + defaultMessage: + 'Graph is unavailable - license information is not available at this time.', + } + ), + }; + } + + const graphFeature = license.getFeature('graph'); + if (!graphFeature.isEnabled) { + return { + showAppLink: false, + enableAppLink: false, + message: i18n.translate('xpack.graph.serverSideErrors.unavailableGraphErrorMessage', { + defaultMessage: 'Graph is unavailable', + }), + }; + } + + const check = license.check('graph', 'platinum'); + + switch (check.state) { + case LICENSE_CHECK_STATE.Expired: + return { + showAppLink: true, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Invalid: + case LICENSE_CHECK_STATE.Unavailable: + return { + showAppLink: false, + enableAppLink: false, + message: check.message || '', + }; + case LICENSE_CHECK_STATE.Valid: + return { + showAppLink: true, + enableAppLink: true, + message: '', + }; + default: + return assertNever(check.state); + } +} diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json new file mode 100644 index 00000000000000..0d0ddc55a391b2 --- /dev/null +++ b/x-pack/plugins/graph/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "graph", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": true, + "requiredPlugins": ["licensing"], + "optionalPlugins": ["home"] +} diff --git a/x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js b/x-pack/plugins/graph/public/index.ts similarity index 73% rename from x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js rename to x-pack/plugins/graph/public/index.ts index 8474f8a614bfb3..ac9ca960c0c7f9 100644 --- a/x-pack/legacy/plugins/watcher/server/routes/api/fields/index.js +++ b/x-pack/plugins/graph/public/index.ts @@ -4,4 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerFieldsRoutes } from './register_fields_routes'; +import { GraphPlugin } from './plugin'; + +export const plugin = () => new GraphPlugin(); diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts new file mode 100644 index 00000000000000..c0cec14e04d611 --- /dev/null +++ b/x-pack/plugins/graph/public/plugin.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import { Plugin } from 'src/core/public'; +import { toggleNavLink } from './services/toggle_nav_link'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { checkLicense } from '../common/check_license'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; + +export interface GraphPluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; +} + +export class GraphPlugin implements Plugin { + private licensing: LicensingPluginSetup | null = null; + + setup(core: CoreSetup, { licensing, home }: GraphPluginSetupDependencies) { + this.licensing = licensing; + + if (home) { + home.featureCatalogue.register({ + id: 'graph', + title: 'Graph', + description: i18n.translate('xpack.graph.pluginDescription', { + defaultMessage: 'Surface and analyze relevant relationships in your Elasticsearch data.', + }), + icon: 'graphApp', + path: '/app/graph', + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, + }); + } + } + + start(core: CoreStart) { + if (this.licensing === null) { + throw new Error('Start called before setup'); + } + this.licensing.license$.subscribe(license => { + toggleNavLink(checkLicense(license), core.chrome.navLinks); + }); + } + + stop() {} +} diff --git a/x-pack/plugins/graph/public/services/toggle_nav_link.ts b/x-pack/plugins/graph/public/services/toggle_nav_link.ts new file mode 100644 index 00000000000000..be917677d311f6 --- /dev/null +++ b/x-pack/plugins/graph/public/services/toggle_nav_link.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 { ChromeNavLink, ChromeNavLinks } from 'kibana/public'; +import { GraphLicenseInformation } from '../../common/check_license'; + +type Mutable = { -readonly [P in keyof T]: T[P] }; +type ChromeNavLinkUpdate = Mutable>; + +export function toggleNavLink( + licenseInformation: GraphLicenseInformation, + navLinks: ChromeNavLinks +) { + const navLinkUpdates: ChromeNavLinkUpdate = { + hidden: !licenseInformation.showAppLink, + }; + if (licenseInformation.showAppLink) { + navLinkUpdates.disabled = !licenseInformation.enableAppLink; + navLinkUpdates.tooltip = licenseInformation.message; + } + + navLinks.update('graph', navLinkUpdates); +} diff --git a/x-pack/plugins/graph/server/index.ts b/x-pack/plugins/graph/server/index.ts new file mode 100644 index 00000000000000..ac9ca960c0c7f9 --- /dev/null +++ b/x-pack/plugins/graph/server/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { GraphPlugin } from './plugin'; + +export const plugin = () => new GraphPlugin(); diff --git a/x-pack/plugins/graph/server/lib/license_state.ts b/x-pack/plugins/graph/server/lib/license_state.ts new file mode 100644 index 00000000000000..1f5744e41534da --- /dev/null +++ b/x-pack/plugins/graph/server/lib/license_state.ts @@ -0,0 +1,43 @@ +/* + * 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 Boom from 'boom'; +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; +import { checkLicense, GraphLicenseInformation } from '../../common/check_license'; + +export class LicenseState { + private licenseInformation: GraphLicenseInformation = checkLicense(undefined); + private subscription: Subscription | null = null; + + private updateInformation(license: ILicense | undefined) { + this.licenseInformation = checkLicense(license); + } + + public start(license$: Observable) { + this.subscription = license$.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } +} + +export function verifyApiAccess(licenseState: LicenseState) { + const licenseCheckResults = licenseState.getLicenseInformation(); + + if (licenseCheckResults.showAppLink && licenseCheckResults.enableAppLink) { + return; + } + + throw Boom.forbidden(licenseCheckResults.message); +} diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts new file mode 100644 index 00000000000000..c7ada3af31b76c --- /dev/null +++ b/x-pack/plugins/graph/server/plugin.ts @@ -0,0 +1,32 @@ +/* + * 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 { Plugin, CoreSetup } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { LicenseState } from './lib/license_state'; +import { registerSearchRoute } from './routes/search'; +import { registerExploreRoute } from './routes/explore'; + +export class GraphPlugin implements Plugin { + private licenseState: LicenseState | null = null; + + public async setup(core: CoreSetup, { licensing }: { licensing: LicensingPluginSetup }) { + const licenseState = new LicenseState(); + licenseState.start(licensing.license$); + this.licenseState = licenseState; + + const router = core.http.createRouter(); + registerSearchRoute({ licenseState, router }); + registerExploreRoute({ licenseState, router }); + } + + public start() {} + public stop() { + if (this.licenseState) { + this.licenseState.stop(); + } + } +} diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts new file mode 100644 index 00000000000000..0a5b9f62f12a1c --- /dev/null +++ b/x-pack/plugins/graph/server/routes/explore.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 { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; +import { get } from 'lodash'; +import { LicenseState, verifyApiAccess } from '../lib/license_state'; + +export function registerExploreRoute({ + router, + licenseState, +}: { + router: IRouter; + licenseState: LicenseState; +}) { + router.post( + { + path: '/api/graph/graphExplore', + validate: { + body: schema.object({ + index: schema.string(), + query: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + router.handleLegacyErrors( + async ( + { + core: { + elasticsearch: { + dataClient: { callAsCurrentUser: callCluster }, + }, + }, + }, + request, + response + ) => { + verifyApiAccess(licenseState); + try { + return response.ok({ + body: { + resp: await callCluster('transport.request', { + path: '/' + encodeURIComponent(request.body.index) + '/_graph/explore', + body: request.body.query, + method: 'POST', + query: {}, + }), + }, + }); + } catch (error) { + // Extract known reasons for bad choice of field + const relevantCause = get( + error, + 'body.error.root_cause', + [] as Array<{ type: string; reason: string }> + ).find(cause => { + return ( + cause.reason.includes('Fielddata is disabled on text fields') || + cause.reason.includes('No support for examining floating point') || + cause.reason.includes('Sample diversifying key must be a single valued-field') || + cause.reason.includes('Failed to parse query') || + cause.type === 'parsing_exception' + ); + }); + + if (relevantCause) { + throw Boom.badRequest(relevantCause.reason); + } + + throw Boom.boomify(error); + } + } + ) + ); +} diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts new file mode 100644 index 00000000000000..400cdc4e82b6e6 --- /dev/null +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; +import Boom from 'boom'; +import { LicenseState, verifyApiAccess } from '../lib/license_state'; + +export function registerSearchRoute({ + router, + licenseState, +}: { + router: IRouter; + licenseState: LicenseState; +}) { + router.post( + { + path: '/api/graph/searchProxy', + validate: { + body: schema.object({ + index: schema.string(), + body: schema.object({}, { allowUnknowns: true }), + }), + }, + }, + router.handleLegacyErrors( + async ( + { + core: { + uiSettings: { client: uiSettings }, + elasticsearch: { + dataClient: { callAsCurrentUser: callCluster }, + }, + }, + }, + request, + response + ) => { + verifyApiAccess(licenseState); + const includeFrozen = await uiSettings.get('search:includeFrozen'); + try { + return response.ok({ + body: { + resp: await callCluster('search', { + index: request.body.index, + body: request.body.body, + rest_total_hits_as_int: true, + ignore_throttled: !includeFrozen, + }), + }, + }); + } catch (error) { + throw Boom.boomify(error, { statusCode: error.statusCode || 500 }); + } + } + ) + ); +} diff --git a/x-pack/plugins/licensing/common/has_license_info_changed.test.ts b/x-pack/plugins/licensing/common/has_license_info_changed.test.ts index 08657826a55677..18c23a41530e35 100644 --- a/x-pack/plugins/licensing/common/has_license_info_changed.test.ts +++ b/x-pack/plugins/licensing/common/has_license_info_changed.test.ts @@ -13,6 +13,7 @@ function license({ error, ...customLicense }: { error?: string; [key: string]: a uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiryDateInMillis: 1000, }; diff --git a/x-pack/plugins/licensing/common/license.test.ts b/x-pack/plugins/licensing/common/license.test.ts index 6dbf009deabb78..884327acd778cc 100644 --- a/x-pack/plugins/licensing/common/license.test.ts +++ b/x-pack/plugins/licensing/common/license.test.ts @@ -6,7 +6,7 @@ import { License } from './license'; import { LICENSE_CHECK_STATE } from './types'; -import { licenseMock } from './license.mock'; +import { licenseMock } from './licensing.mock'; describe('License', () => { const basicLicense = licenseMock.create(); diff --git a/x-pack/plugins/licensing/common/license.ts b/x-pack/plugins/licensing/common/license.ts index b8327ac5541071..8423fed1d6a4e6 100644 --- a/x-pack/plugins/licensing/common/license.ts +++ b/x-pack/plugins/licensing/common/license.ts @@ -33,6 +33,7 @@ export class License implements ILicense { public readonly status?: LicenseStatus; public readonly expiryDateInMillis?: number; public readonly type?: LicenseType; + public readonly mode?: LicenseType; public readonly signature: string; /** @@ -65,6 +66,7 @@ export class License implements ILicense { this.status = license.status; this.expiryDateInMillis = license.expiryDateInMillis; this.type = license.type; + this.mode = license.mode; } this.isActive = this.status === 'active'; diff --git a/x-pack/plugins/licensing/common/license_update.test.ts b/x-pack/plugins/licensing/common/license_update.test.ts index 68660eaf2d713e..e714edfbdd88c1 100644 --- a/x-pack/plugins/licensing/common/license_update.test.ts +++ b/x-pack/plugins/licensing/common/license_update.test.ts @@ -9,7 +9,7 @@ import { take, toArray } from 'rxjs/operators'; import { ILicense, LicenseType } from './types'; import { createLicenseUpdate } from './license_update'; -import { licenseMock } from './license.mock'; +import { licenseMock } from './licensing.mock'; const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const stop$ = new Subject(); diff --git a/x-pack/plugins/licensing/common/license.mock.ts b/x-pack/plugins/licensing/common/licensing.mock.ts similarity index 98% rename from x-pack/plugins/licensing/common/license.mock.ts rename to x-pack/plugins/licensing/common/licensing.mock.ts index f04ebeec81bdfc..52721703fcb737 100644 --- a/x-pack/plugins/licensing/common/license.mock.ts +++ b/x-pack/plugins/licensing/common/licensing.mock.ts @@ -19,6 +19,7 @@ function createLicense({ uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiryDateInMillis: 5000, }; diff --git a/x-pack/plugins/licensing/common/types.ts b/x-pack/plugins/licensing/common/types.ts index c5d838d23d8c38..840f90e083d5ee 100644 --- a/x-pack/plugins/licensing/common/types.ts +++ b/x-pack/plugins/licensing/common/types.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable } from 'rxjs'; export enum LICENSE_CHECK_STATE { Unavailable = 'UNAVAILABLE', @@ -57,6 +56,11 @@ export interface PublicLicense { * The license type, being usually one of basic, standard, gold, platinum, or trial. */ type: LicenseType; + /** + * The license type, being usually one of basic, standard, gold, platinum, or trial. + * @deprecated use 'type' instead + */ + mode: LicenseType; } /** @@ -119,6 +123,12 @@ export interface ILicense { */ type?: LicenseType; + /** + * The license type, being usually one of basic, standard, gold, platinum, or trial. + * @deprecated use 'type' instead. + */ + mode?: LicenseType; + /** * Signature of the license content. */ @@ -173,15 +183,3 @@ export interface ILicense { */ getFeature(name: string): LicenseFeature; } - -/** @public */ -export interface LicensingPluginSetup { - /** - * Steam of licensing information {@link ILicense}. - */ - license$: Observable; - /** - * Triggers licensing information re-fetch. - */ - refresh(): Promise; -} diff --git a/x-pack/plugins/licensing/public/index.ts b/x-pack/plugins/licensing/public/index.ts index 32e911bb2cdd2b..e19ebe7a684182 100644 --- a/x-pack/plugins/licensing/public/index.ts +++ b/x-pack/plugins/licensing/public/index.ts @@ -8,4 +8,5 @@ import { PluginInitializerContext } from 'src/core/public'; import { LicensingPlugin } from './plugin'; export * from '../common/types'; +export * from './types'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); diff --git a/x-pack/plugins/licensing/public/licensing.mock.ts b/x-pack/plugins/licensing/public/licensing.mock.ts new file mode 100644 index 00000000000000..e2ed070017847e --- /dev/null +++ b/x-pack/plugins/licensing/public/licensing.mock.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 { BehaviorSubject } from 'rxjs'; +import { LicensingPluginSetup } from './types'; +import { licenseMock } from '../common/licensing.mock'; + +const createSetupMock = () => { + const license = licenseMock.create(); + const mock: jest.Mocked = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + }; + mock.refresh.mockResolvedValue(license); + + return mock; +}; + +export const licensingMock = { + createSetup: createSetupMock, + createLicense: licenseMock.create, +}; diff --git a/x-pack/plugins/licensing/public/plugin.test.ts b/x-pack/plugins/licensing/public/plugin.test.ts index c356f7f5df1848..4469f26836b185 100644 --- a/x-pack/plugins/licensing/public/plugin.test.ts +++ b/x-pack/plugins/licensing/public/plugin.test.ts @@ -11,12 +11,10 @@ import { LicenseType } from '../common/types'; import { LicensingPlugin, licensingSessionStorageKey } from './plugin'; import { License } from '../common/license'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; import { coreMock } from '../../../../src/core/public/mocks'; import { HttpInterceptor } from 'src/core/public'; -const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - describe('licensing plugin', () => { let plugin: LicensingPlugin; @@ -34,15 +32,7 @@ describe('licensing plugin', () => { const coreSetup = coreMock.createSetup(); const firstLicense = licenseMock.create({ license: { uid: 'first', type: 'basic' } }); const secondLicense = licenseMock.create({ license: { uid: 'second', type: 'gold' } }); - coreSetup.http.get - .mockImplementationOnce(async () => { - await delay(100); - return firstLicense; - }) - .mockImplementationOnce(async () => { - await delay(100); - return secondLicense; - }); + coreSetup.http.get.mockResolvedValueOnce(firstLicense).mockResolvedValueOnce(secondLicense); const { license$, refresh } = await plugin.setup(coreSetup); @@ -147,7 +137,7 @@ describe('licensing plugin', () => { expect(sessionStorage.setItem.mock.calls[0][0]).toBe(licensingSessionStorageKey); expect(sessionStorage.setItem.mock.calls[0][1]).toMatchInlineSnapshot( - `"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"` + `"{\\"license\\":{\\"uid\\":\\"fresh\\",\\"status\\":\\"active\\",\\"type\\":\\"basic\\",\\"mode\\":\\"basic\\",\\"expiryDateInMillis\\":5000},\\"features\\":{\\"ccr\\":{\\"isEnabled\\":true,\\"isAvailable\\":true},\\"ml\\":{\\"isEnabled\\":false,\\"isAvailable\\":true}},\\"signature\\":\\"xxxxxxxxx\\"}"` ); const saved = JSON.parse(sessionStorage.setItem.mock.calls[0][1]); diff --git a/x-pack/plugins/licensing/public/plugin.ts b/x-pack/plugins/licensing/public/plugin.ts index c0dc0f21b90bed..7d2498b0f7ff6e 100644 --- a/x-pack/plugins/licensing/public/plugin.ts +++ b/x-pack/plugins/licensing/public/plugin.ts @@ -7,7 +7,8 @@ import { Subject, Subscription } from 'rxjs'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { ILicense, LicensingPluginSetup } from '../common/types'; +import { ILicense } from '../common/types'; +import { LicensingPluginSetup } from './types'; import { createLicenseUpdate } from '../common/license_update'; import { License } from '../common/license'; import { mountExpiredBanner } from './expired_banner'; diff --git a/x-pack/plugins/licensing/public/types.ts b/x-pack/plugins/licensing/public/types.ts new file mode 100644 index 00000000000000..df8e50be5d1507 --- /dev/null +++ b/x-pack/plugins/licensing/public/types.ts @@ -0,0 +1,20 @@ +/* + * 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 { Observable } from 'rxjs'; + +import { ILicense } from '../common/types'; + +/** @public */ +export interface LicensingPluginSetup { + /** + * Steam of licensing information {@link ILicense}. + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + */ + refresh(): Promise; +} diff --git a/x-pack/plugins/licensing/server/index.ts b/x-pack/plugins/licensing/server/index.ts index fff9ccc296ce3f..0e14ead7c6c575 100644 --- a/x-pack/plugins/licensing/server/index.ts +++ b/x-pack/plugins/licensing/server/index.ts @@ -10,4 +10,5 @@ import { LicensingPlugin } from './plugin'; export const plugin = (context: PluginInitializerContext) => new LicensingPlugin(context); export * from '../common/types'; +export * from './types'; export { config } from './licensing_config'; diff --git a/x-pack/plugins/licensing/server/licensing.mock.ts b/x-pack/plugins/licensing/server/licensing.mock.ts new file mode 100644 index 00000000000000..b2059e36fd0c03 --- /dev/null +++ b/x-pack/plugins/licensing/server/licensing.mock.ts @@ -0,0 +1,29 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { LicensingPluginSetup } from './types'; +import { licenseMock } from '../common/licensing.mock'; + +const createSetupMock = () => { + const license = licenseMock.create(); + const mock: jest.Mocked = { + license$: new BehaviorSubject(license), + refresh: jest.fn(), + createLicensePoller: jest.fn(), + }; + mock.refresh.mockResolvedValue(license); + mock.createLicensePoller.mockReturnValue({ + license$: mock.license$, + refresh: mock.refresh, + }); + + return mock; +}; + +export const licensingMock = { + createSetup: createSetupMock, + createLicense: licenseMock.create, +}; diff --git a/x-pack/plugins/licensing/server/licensing_config.ts b/x-pack/plugins/licensing/server/licensing_config.ts index 6cb3e8d9ef3a19..d218b643812799 100644 --- a/x-pack/plugins/licensing/server/licensing_config.ts +++ b/x-pack/plugins/licensing/server/licensing_config.ts @@ -5,11 +5,22 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'kibana/server'; -export const config = { +const configSchema = schema.object({ + api_polling_frequency: schema.duration({ defaultValue: '30s' }), +}); + +export type LicenseConfigType = TypeOf; + +export const config: PluginConfigDescriptor = { schema: schema.object({ - pollingFrequency: schema.duration({ defaultValue: '30s' }), + api_polling_frequency: schema.duration({ defaultValue: '30s' }), }), + deprecations: ({ renameFromRoot }) => [ + renameFromRoot( + 'xpack.xpack_main.xpack_api_polling_frequency_millis', + 'xpack.licensing.api_polling_frequency' + ), + ], }; - -export type LicenseConfigType = TypeOf; diff --git a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts index 82af786482d582..20e7f34c3ce3c4 100644 --- a/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts +++ b/x-pack/plugins/licensing/server/licensing_route_handler_context.test.ts @@ -5,7 +5,7 @@ */ import { BehaviorSubject } from 'rxjs'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; import { createRouteHandlerContext } from './licensing_route_handler_context'; diff --git a/x-pack/plugins/licensing/server/mocks.ts b/x-pack/plugins/licensing/server/mocks.ts new file mode 100644 index 00000000000000..237636d1630178 --- /dev/null +++ b/x-pack/plugins/licensing/server/mocks.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 * from './licensing.mock'; diff --git a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts index 4251e72accc9fa..9acfcef0ac8df9 100644 --- a/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts +++ b/x-pack/plugins/licensing/server/on_pre_response_handler.test.ts @@ -6,7 +6,7 @@ import { BehaviorSubject } from 'rxjs'; import { createOnPreResponseHandler } from './on_pre_response_handler'; import { httpServiceMock, httpServerMock } from '../../../../src/core/server/mocks'; -import { licenseMock } from '../common/license.mock'; +import { licenseMock } from '../common/licensing.mock'; describe('createOnPreResponseHandler', () => { it('sets license.signature header immediately for non-error responses', async () => { diff --git a/x-pack/plugins/licensing/server/plugin.test.ts b/x-pack/plugins/licensing/server/plugin.test.ts index 62b6ec6a106b78..0b5a3533bd3b69 100644 --- a/x-pack/plugins/licensing/server/plugin.test.ts +++ b/x-pack/plugins/licensing/server/plugin.test.ts @@ -21,11 +21,11 @@ function buildRawLicense(options: Partial = {}): RawLicense { uid: 'uid-000000001234', status: 'active', type: 'basic', + mode: 'basic', expiry_date_in_millis: 1000, }; return Object.assign(defaultRawLicense, options); } -const pollingFrequency = moment.duration(100); const flushPromises = (ms = 50) => new Promise(res => setTimeout(res, ms)); @@ -37,7 +37,7 @@ describe('licensing plugin', () => { beforeEach(() => { pluginInitContextMock = coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }); plugin = new LicensingPlugin(pluginInitContextMock); }); @@ -200,7 +200,7 @@ describe('licensing plugin', () => { plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ // disable polling mechanism - pollingFrequency: moment.duration(50000), + api_polling_frequency: moment.duration(50000), }) ); const dataClient = elasticsearchServiceMock.createClusterClient(); @@ -222,13 +222,88 @@ describe('licensing plugin', () => { }); }); + describe('#createLicensePoller', () => { + let plugin: LicensingPlugin; + + afterEach(async () => { + await plugin.stop(); + }); + + it(`creates a poller fetching license from passed 'clusterClient' every 'api_polling_frequency' ms`, async () => { + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ + api_polling_frequency: moment.duration(50000), + }) + ); + + const dataClient = elasticsearchServiceMock.createClusterClient(); + dataClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense(), + features: {}, + }); + const coreSetup = coreMock.createSetup(); + coreSetup.elasticsearch.dataClient$ = new BehaviorSubject(dataClient); + + const { createLicensePoller, license$ } = await plugin.setup(coreSetup); + const customClient = elasticsearchServiceMock.createClusterClient(); + customClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense({ type: 'gold' }), + features: {}, + }); + + const customPollingFrequency = 100; + const { license$: customLicense$ } = createLicensePoller( + customClient, + customPollingFrequency + ); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + + const customLicense = await customLicense$.pipe(take(1)).toPromise(); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + + await flushPromises(customPollingFrequency * 1.5); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(2); + + expect(customLicense.isAvailable).toBe(true); + expect(customLicense.type).toBe('gold'); + + expect(await license$.pipe(take(1)).toPromise()).not.toBe(customLicense); + }); + + it('creates a poller with a manual refresh control', async () => { + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ + api_polling_frequency: moment.duration(100), + }) + ); + + const coreSetup = coreMock.createSetup(); + const { createLicensePoller } = await plugin.setup(coreSetup); + + const customClient = elasticsearchServiceMock.createClusterClient(); + customClient.callAsInternalUser.mockResolvedValue({ + license: buildRawLicense({ type: 'gold' }), + features: {}, + }); + + const { license$, refresh } = createLicensePoller(customClient, 10000); + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(0); + + await refresh(); + + expect(customClient.callAsInternalUser).toHaveBeenCalledTimes(1); + const license = await license$.pipe(take(1)).toPromise(); + expect(license.type).toBe('gold'); + }); + }); + describe('extends core contexts', () => { let plugin: LicensingPlugin; beforeEach(() => { plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }) ); }); @@ -257,7 +332,9 @@ describe('licensing plugin', () => { let plugin: LicensingPlugin; beforeEach(() => { - plugin = new LicensingPlugin(coreMock.createPluginInitializerContext({ pollingFrequency })); + plugin = new LicensingPlugin( + coreMock.createPluginInitializerContext({ api_polling_frequency: moment.duration(100) }) + ); }); afterEach(async () => { @@ -278,7 +355,7 @@ describe('licensing plugin', () => { it('stops polling', async () => { const plugin = new LicensingPlugin( coreMock.createPluginInitializerContext({ - pollingFrequency, + api_polling_frequency: moment.duration(100), }) ); const coreSetup = coreMock.createSetup(); diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 64f7cc56948f2c..2eabd534a997c5 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -6,7 +6,7 @@ import { Observable, Subject, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; -import moment, { Duration } from 'moment'; +import moment from 'moment'; import { createHash } from 'crypto'; import stringify from 'json-stable-stringify'; @@ -19,7 +19,8 @@ import { IClusterClient, } from 'src/core/server'; -import { ILicense, LicensingPluginSetup, PublicLicense, PublicFeatures } from '../common/types'; +import { ILicense, PublicLicense, PublicFeatures } from '../common/types'; +import { LicensingPluginSetup } from './types'; import { License } from '../common/license'; import { createLicenseUpdate } from '../common/license_update'; @@ -34,6 +35,7 @@ function normalizeServerLicense(license: RawLicense): PublicLicense { return { uid: license.uid, type: license.type, + mode: license.mode, expiryDateInMillis: license.expiry_date_in_millis, status: license.status, }; @@ -89,9 +91,13 @@ export class LicensingPlugin implements Plugin { public async setup(core: CoreSetup) { this.logger.debug('Setting up Licensing plugin'); const config = await this.config$.pipe(take(1)).toPromise(); + const pollingFrequency = config.api_polling_frequency; const dataClient = await core.elasticsearch.dataClient$.pipe(take(1)).toPromise(); - const { refresh, license$ } = this.createLicensePoller(dataClient, config.pollingFrequency); + const { refresh, license$ } = this.createLicensePoller( + dataClient, + pollingFrequency.asMilliseconds() + ); core.http.registerRouteHandlerContext('licensing', createRouteHandlerContext(license$)); @@ -101,11 +107,14 @@ export class LicensingPlugin implements Plugin { return { refresh, license$, + createLicensePoller: this.createLicensePoller.bind(this), }; } - private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: Duration) { - const intervalRefresh$ = timer(0, pollingFrequency.asMilliseconds()); + private createLicensePoller(clusterClient: IClusterClient, pollingFrequency: number) { + this.logger.debug(`Polling Elasticsearch License API with frequency ${pollingFrequency}ms.`); + + const intervalRefresh$ = timer(0, pollingFrequency); const { license$, refreshManually } = createLicenseUpdate(intervalRefresh$, this.stop$, () => this.fetchLicense(clusterClient) diff --git a/x-pack/plugins/licensing/server/types.ts b/x-pack/plugins/licensing/server/types.ts index d553f090fb6486..f46167a0d0a42f 100644 --- a/x-pack/plugins/licensing/server/types.ts +++ b/x-pack/plugins/licensing/server/types.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; +import { IClusterClient } from 'src/core/server'; import { ILicense, LicenseStatus, LicenseType } from '../common/types'; export interface ElasticsearchError extends Error { @@ -34,6 +36,7 @@ export interface RawLicense { status: LicenseStatus; expiry_date_in_millis: number; type: LicenseType; + mode: LicenseType; } declare module 'src/core/server' { @@ -43,3 +46,25 @@ declare module 'src/core/server' { }; } } + +/** @public */ +export interface LicensingPluginSetup { + /** + * Steam of licensing information {@link ILicense}. + */ + license$: Observable; + /** + * Triggers licensing information re-fetch. + */ + refresh(): Promise; + + /** + * Creates a license poller to retrieve a license data with. + * Allows a plugin to configure a cluster to retrieve data from at + * given polling frequency. + */ + createLicensePoller: ( + clusterClient: IClusterClient, + pollingFrequency: number + ) => { license$: Observable; refresh(): Promise }; +} diff --git a/x-pack/legacy/plugins/security/common/model/api_key.ts b/x-pack/plugins/security/common/model/api_key.ts similarity index 100% rename from x-pack/legacy/plugins/security/common/model/api_key.ts rename to x-pack/plugins/security/common/model/api_key.ts diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index c6ccd2518d2610..226ea3b70afe2a 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { ApiKey, ApiKeyToInvalidate } from './api_key'; export { User, EditUser, getUserDisplayName } from './user'; export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; diff --git a/x-pack/plugins/security/public/index.ts b/x-pack/plugins/security/public/index.ts index 12a3092039d0de..dc34fcbbe7d1e6 100644 --- a/x-pack/plugins/security/public/index.ts +++ b/x-pack/plugins/security/public/index.ts @@ -6,6 +6,7 @@ import { PluginInitializer } from 'src/core/public'; import { SecurityPlugin, SecurityPluginSetup, SecurityPluginStart } from './plugin'; +export { SessionInfo } from './types'; export const plugin: PluginInitializer = () => new SecurityPlugin(); diff --git a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts index 81625e1753b273..8a2251f3f7f7c0 100644 --- a/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts +++ b/x-pack/plugins/security/public/session/session_timeout_http_interceptor.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpInterceptor, HttpErrorResponse, HttpResponse, IAnonymousPaths } from 'src/core/public'; +import { + HttpInterceptor, + HttpErrorResponse, + IHttpResponse, + IAnonymousPaths, +} from 'src/core/public'; import { ISessionTimeout } from './session_timeout'; @@ -15,7 +20,7 @@ const isSystemAPIRequest = (request: Request) => { export class SessionTimeoutHttpInterceptor implements HttpInterceptor { constructor(private sessionTimeout: ISessionTimeout, private anonymousPaths: IAnonymousPaths) {} - response(httpResponse: HttpResponse) { + response(httpResponse: IHttpResponse) { if (this.anonymousPaths.isAnonymous(window.location.pathname)) { return; } diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index a81246c8f78b04..dd580c890bf94e 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -7,6 +7,7 @@ jest.mock('./providers/basic', () => ({ BasicAuthenticationProvider: jest.fn() })); import Boom from 'boom'; +import { duration, Duration } from 'moment'; import { SessionStorage } from '../../../../../src/core/server'; import { @@ -439,7 +440,7 @@ describe('Authenticator', () => { // Create new authenticator with non-null session `idleTimeout`. mockOptions = getMockOptions({ session: { - idleTimeout: 3600 * 24, + idleTimeout: duration(3600 * 24), lifespan: null, }, authc: { providers: ['basic'], oidc: {}, saml: {} }, @@ -478,8 +479,8 @@ describe('Authenticator', () => { // Create new authenticator with non-null session `idleTimeout` and `lifespan`. mockOptions = getMockOptions({ session: { - idleTimeout: hr * 2, - lifespan: hr * 8, + idleTimeout: duration(hr * 2), + lifespan: duration(hr * 8), }, authc: { providers: ['basic'], oidc: {}, saml: {} }, }); @@ -520,7 +521,7 @@ describe('Authenticator', () => { const currentDate = new Date(Date.UTC(2019, 10, 10)).valueOf(); async function createAndUpdateSession( - lifespan: number | null, + lifespan: Duration | null, oldExpiration: number | null, newExpiration: number | null ) { @@ -564,7 +565,7 @@ describe('Authenticator', () => { } it('does not change a non-null lifespan expiration when configured to non-null value.', async () => { - await createAndUpdateSession(hr * 8, 1234, 1234); + await createAndUpdateSession(duration(hr * 8), 1234, 1234); }); it('does not change a null lifespan expiration when configured to null value.', async () => { await createAndUpdateSession(null, null, null); @@ -573,7 +574,7 @@ describe('Authenticator', () => { await createAndUpdateSession(null, 1234, null); }); it('does change a null lifespan expiration when configured to non-null value', async () => { - await createAndUpdateSession(hr * 8, null, currentDate + hr * 8); + await createAndUpdateSession(duration(hr * 8), null, currentDate + hr * 8); }); }); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index 8f947349cb2e86..be952a154cee49 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Duration } from 'moment'; import { SessionStorageFactory, SessionStorage, @@ -31,7 +32,7 @@ import { import { AuthenticationResult } from './authentication_result'; import { DeauthenticationResult } from './deauthentication_result'; import { Tokens } from './tokens'; -import { SessionInfo } from '../../public/types'; +import { SessionInfo } from '../../public'; /** * The shape of the session that is actually stored in the cookie. @@ -172,12 +173,12 @@ export class Authenticator { /** * Session timeout in ms. If `null` session will stay active until the browser is closed. */ - private readonly idleTimeout: number | null = null; + private readonly idleTimeout: Duration | null = null; /** * Session max lifespan in ms. If `null` session may live indefinitely. */ - private readonly lifespan: number | null = null; + private readonly lifespan: Duration | null = null; /** * Internal authenticator logger. @@ -225,7 +226,6 @@ export class Authenticator { ); this.serverBasePath = this.options.basePath.serverBasePath || '/'; - // only set these vars if they are defined in options (otherwise coalesce to existing/default) this.idleTimeout = this.options.config.session.idleTimeout; this.lifespan = this.options.config.session.lifespan; } @@ -492,11 +492,16 @@ export class Authenticator { private calculateExpiry( existingSession: ProviderSession | null ): { idleTimeoutExpiration: number | null; lifespanExpiration: number | null } { - let lifespanExpiration = this.lifespan && Date.now() + this.lifespan; - if (existingSession && existingSession.lifespanExpiration && this.lifespan) { - lifespanExpiration = existingSession.lifespanExpiration; - } - const idleTimeoutExpiration = this.idleTimeout && Date.now() + this.idleTimeout; + const now = Date.now(); + // if we are renewing an existing session, use its `lifespanExpiration` -- otherwise, set this value + // based on the configured server `lifespan`. + // note, if the server had a `lifespan` set and then removes it, remove `lifespanExpiration` on renewed sessions + // also, if the server did not have a `lifespan` set and then adds it, add `lifespanExpiration` on renewed sessions + const lifespanExpiration = + existingSession?.lifespanExpiration && this.lifespan + ? existingSession.lifespanExpiration + : this.lifespan && now + this.lifespan.asMilliseconds(); + const idleTimeoutExpiration = this.idleTimeout && now + this.idleTimeout.asMilliseconds(); return { idleTimeoutExpiration, lifespanExpiration }; } diff --git a/x-pack/plugins/security/server/authentication/index.test.ts b/x-pack/plugins/security/server/authentication/index.test.ts index ff7cf876adbef2..6a0057e97dcf06 100644 --- a/x-pack/plugins/security/server/authentication/index.test.ts +++ b/x-pack/plugins/security/server/authentication/index.test.ts @@ -60,6 +60,10 @@ describe('setupAuthentication()', () => { coreMock.createPluginInitializerContext({ encryptionKey: 'ab'.repeat(16), secureCookies: true, + session: { + idleTimeout: null, + lifespan: null, + }, cookieName: 'my-sid-cookie', authc: { providers: ['basic'] }, }), @@ -87,7 +91,6 @@ describe('setupAuthentication()', () => { encryptionKey: 'ab'.repeat(16), secureCookies: true, cookieName: 'my-sid-cookie', - authc: { providers: ['basic'] }, }; await setupAuthentication(mockSetupAuthenticationParams); diff --git a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts index c1d7dcca4c78ff..ad7eab76db088d 100644 --- a/x-pack/plugins/security/server/authentication/providers/oidc.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/oidc.test.ts @@ -42,7 +42,7 @@ describe('OIDCAuthenticationProvider', () => { describe('`login` method', () => { it('redirects third party initiated login attempts to the OpenId Connect Provider.', async () => { - const request = httpServerMock.createKibanaRequest({ path: '/api/security/v1/oidc' }); + const request = httpServerMock.createKibanaRequest({ path: '/api/security/oidc' }); mockOptions.client.callAsInternalUser.withArgs('shield.oidcPrepare').resolves({ state: 'statevalue', @@ -205,13 +205,13 @@ describe('OIDCAuthenticationProvider', () => { describe('authorization code flow', () => { defineAuthenticationFlowTests(() => ({ request: httpServerMock.createKibanaRequest({ - path: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + path: '/api/security/oidc?code=somecodehere&state=somestatehere', }), attempt: { flow: OIDCAuthenticationFlow.AuthorizationCode, - authenticationResponseURI: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + authenticationResponseURI: '/api/security/oidc?code=somecodehere&state=somestatehere', }, - expectedRedirectURI: '/api/security/v1/oidc?code=somecodehere&state=somestatehere', + expectedRedirectURI: '/api/security/oidc?code=somecodehere&state=somestatehere', })); }); @@ -219,14 +219,13 @@ describe('OIDCAuthenticationProvider', () => { defineAuthenticationFlowTests(() => ({ request: httpServerMock.createKibanaRequest({ path: - '/api/security/v1/oidc?authenticationResponseURI=http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + '/api/security/oidc?authenticationResponseURI=http://kibana/api/security/oidc/implicit#id_token=sometoken', }), attempt: { flow: OIDCAuthenticationFlow.Implicit, - authenticationResponseURI: - 'http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + authenticationResponseURI: 'http://kibana/api/security/oidc/implicit#id_token=sometoken', }, - expectedRedirectURI: 'http://kibana/api/security/v1/oidc/implicit#id_token=sometoken', + expectedRedirectURI: 'http://kibana/api/security/oidc/implicit#id_token=sometoken', })); }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index b1cb78008da008..8c1241937892e1 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -48,7 +48,7 @@ describe('#atSpace', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); @@ -291,7 +291,7 @@ describe('#atSpaces', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); @@ -772,7 +772,7 @@ describe('#globally', () => { const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( mockActions, mockClusterClient, - () => application + application ); const request = httpServerMock.createKibanaRequest(); const checkPrivileges = checkPrivilegesWithRequest(request); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 5bc3ce075452d4..3ef7a8f29a0bf2 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -61,7 +61,7 @@ export interface CheckPrivileges { export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, clusterClient: IClusterClient, - getApplicationName: () => string + applicationName: string ) { const hasIncompatibleVersion = ( applicationPrivilegesResponse: HasPrivilegesResponseApplication @@ -81,23 +81,24 @@ export function checkPrivilegesWithRequestFactory( : [privilegeOrPrivileges]; const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); - const application = getApplicationName(); const hasPrivilegesResponse = (await clusterClient .asScoped(request) .callAsCurrentUser('shield.hasPrivileges', { body: { - applications: [{ application, resources, privileges: allApplicationPrivileges }], + applications: [ + { application: applicationName, resources, privileges: allApplicationPrivileges }, + ], }, })) as HasPrivilegesResponse; validateEsPrivilegeResponse( hasPrivilegesResponse, - application, + applicationName, allApplicationPrivileges, resources ); - const applicationPrivilegesResponse = hasPrivilegesResponse.application[application]; + const applicationPrivilegesResponse = hasPrivilegesResponse.application[applicationName]; if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 2e700745c69dcc..930ede4157723a 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -8,12 +8,15 @@ import { Actions } from '.'; import { AuthorizationMode } from './mode'; export const authorizationMock = { - create: ({ version = 'mock-version' }: { version?: string } = {}) => ({ + create: ({ + version = 'mock-version', + applicationName = 'mock-application', + }: { version?: string; applicationName?: string } = {}) => ({ actions: new Actions(version), checkPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), - getApplicationName: jest.fn().mockReturnValue('mock-application'), + applicationName, mode: { useRbacForRequest: jest.fn() } as jest.Mocked, privileges: { get: jest.fn() }, registerPrivilegesWithCluster: jest.fn(), diff --git a/x-pack/plugins/security/server/authorization/index.test.ts b/x-pack/plugins/security/server/authorization/index.test.ts index 24179e062230a9..34b9efea771659 100644 --- a/x-pack/plugins/security/server/authorization/index.test.ts +++ b/x-pack/plugins/security/server/authorization/index.test.ts @@ -53,7 +53,6 @@ test(`returns exposed services`, () => { .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }); const mockFeaturesService = { getFeatures: () => [] }; - const mockGetLegacyAPI = () => ({ kibanaIndexName }); const mockLicense = licenseMock.create(); const authz = setupAuthorization({ @@ -61,20 +60,20 @@ test(`returns exposed services`, () => { clusterClient: mockClusterClient, license: mockLicense, loggers: loggingServiceMock.create(), - getLegacyAPI: mockGetLegacyAPI, + kibanaIndexName, packageVersion: 'some-version', featuresService: mockFeaturesService, getSpacesService: mockGetSpacesService, }); expect(authz.actions.version).toBe('version:some-version'); - expect(authz.getApplicationName()).toBe(application); + expect(authz.applicationName).toBe(application); expect(authz.checkPrivilegesWithRequest).toBe(mockCheckPrivilegesWithRequest); expect(checkPrivilegesWithRequestFactory).toHaveBeenCalledWith( authz.actions, mockClusterClient, - authz.getApplicationName + authz.applicationName ); expect(authz.checkPrivilegesDynamicallyWithRequest).toBe( diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index b5f9efadbd8d0e..41e6d12eb8f365 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -12,7 +12,7 @@ import { IClusterClient, } from '../../../../../src/core/server'; -import { FeaturesService, LegacyAPI, SpacesService } from '../plugin'; +import { FeaturesService, SpacesService } from '../plugin'; import { Actions } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; import { @@ -43,7 +43,7 @@ interface SetupAuthorizationParams { license: SecurityLicense; loggers: LoggerFactory; featuresService: FeaturesService; - getLegacyAPI(): Pick; + kibanaIndexName: string; getSpacesService(): SpacesService | undefined; } @@ -52,7 +52,7 @@ export interface Authorization { checkPrivilegesWithRequest: CheckPrivilegesWithRequest; checkPrivilegesDynamicallyWithRequest: CheckPrivilegesDynamicallyWithRequest; checkSavedObjectsPrivilegesWithRequest: CheckSavedObjectsPrivilegesWithRequest; - getApplicationName: () => string; + applicationName: string; mode: AuthorizationMode; privileges: PrivilegesService; disableUnauthorizedCapabilities: ( @@ -69,23 +69,23 @@ export function setupAuthorization({ license, loggers, featuresService, - getLegacyAPI, + kibanaIndexName, getSpacesService, }: SetupAuthorizationParams): Authorization { const actions = new Actions(packageVersion); const mode = authorizationModeFactory(license); - const getApplicationName = () => `${APPLICATION_PREFIX}${getLegacyAPI().kibanaIndexName}`; + const applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( actions, clusterClient, - getApplicationName + applicationName ); const privileges = privilegesFactory(actions, featuresService); const logger = loggers.get('authorization'); const authz = { actions, - getApplicationName, + applicationName, checkPrivilegesWithRequest, checkPrivilegesDynamicallyWithRequest: checkPrivilegesDynamicallyWithRequestFactory( checkPrivilegesWithRequest, @@ -123,7 +123,7 @@ export function setupAuthorization({ registerPrivilegesWithCluster: async () => { validateFeaturePrivileges(actions, featuresService.getFeatures()); - await registerPrivilegesWithCluster(logger, privileges, getApplicationName(), clusterClient); + await registerPrivilegesWithCluster(logger, privileges, applicationName, clusterClient); }, }; diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 9ddb3e6e96b90b..f7374eedb5520d 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -254,19 +254,22 @@ describe('config schema', () => { }); describe('createConfig$()', () => { + const mockAndCreateConfig = async (isTLSEnabled: boolean, value = {}, context?: any) => { + const contextMock = coreMock.createPluginInitializerContext( + // we must use validate to avoid errors in `createConfig$` + ConfigSchema.validate(value, context) + ); + return await createConfig$(contextMock, isTLSEnabled) + .pipe(first()) + .toPromise() + .then(config => ({ contextMock, config })); + }; it('should log a warning and set xpack.security.encryptionKey if not set', async () => { const mockRandomBytes = jest.requireMock('crypto').randomBytes; mockRandomBytes.mockReturnValue('ab'.repeat(16)); - const contextMock = coreMock.createPluginInitializerContext({}); - const config = await createConfig$(contextMock, true) - .pipe(first()) - .toPromise(); - expect(config).toEqual({ - encryptionKey: 'ab'.repeat(16), - secureCookies: true, - session: { idleTimeout: null, lifespan: null }, - }); + const { contextMock, config } = await mockAndCreateConfig(true, {}, { dist: true }); + expect(config.encryptionKey).toEqual('ab'.repeat(16)); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` Array [ @@ -278,14 +281,7 @@ describe('createConfig$()', () => { }); it('should log a warning if SSL is not configured', async () => { - const contextMock = coreMock.createPluginInitializerContext({ - encryptionKey: 'a'.repeat(32), - secureCookies: false, - }); - - const config = await createConfig$(contextMock, false) - .pipe(first()) - .toPromise(); + const { contextMock, config } = await mockAndCreateConfig(false, {}); expect(config.secureCookies).toEqual(false); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` @@ -298,14 +294,7 @@ describe('createConfig$()', () => { }); it('should log a warning if SSL is not configured yet secure cookies are being used', async () => { - const contextMock = coreMock.createPluginInitializerContext({ - encryptionKey: 'a'.repeat(32), - secureCookies: true, - }); - - const config = await createConfig$(contextMock, false) - .pipe(first()) - .toPromise(); + const { contextMock, config } = await mockAndCreateConfig(false, { secureCookies: true }); expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toMatchInlineSnapshot(` @@ -318,14 +307,7 @@ describe('createConfig$()', () => { }); it('should set xpack.security.secureCookies if SSL is configured', async () => { - const contextMock = coreMock.createPluginInitializerContext({ - encryptionKey: 'a'.repeat(32), - secureCookies: false, - }); - - const config = await createConfig$(contextMock, true) - .pipe(first()) - .toPromise(); + const { contextMock, config } = await mockAndCreateConfig(true, {}); expect(config.secureCookies).toEqual(true); expect(loggingServiceMock.collect(contextMock.logger).warn).toEqual([]); diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index c7d990f81369ec..b3f96497b0538b 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -8,6 +8,7 @@ import crypto from 'crypto'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { schema, Type, TypeOf } from '@kbn/config-schema'; +import { duration } from 'moment'; import { PluginInitializerContext } from '../../../../src/core/server'; export type ConfigType = ReturnType extends Observable @@ -34,10 +35,10 @@ export const ConfigSchema = schema.object( schema.maybe(schema.string({ minLength: 32 })), schema.string({ minLength: 32, defaultValue: 'a'.repeat(32) }) ), - sessionTimeout: schema.maybe(schema.oneOf([schema.number(), schema.literal(null)])), // DEPRECATED + sessionTimeout: schema.maybe(schema.nullable(schema.number())), // DEPRECATED session: schema.object({ - idleTimeout: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), - lifespan: schema.oneOf([schema.number(), schema.literal(null)], { defaultValue: null }), + idleTimeout: schema.nullable(schema.duration()), + lifespan: schema.nullable(schema.duration()), }), secureCookies: schema.boolean({ defaultValue: false }), authc: schema.object({ @@ -90,17 +91,16 @@ export function createConfig$(context: PluginInitializerContext, isTLSEnabled: b // "sessionTimeout" is deprecated and replaced with "session.idleTimeout" // however, NP does not yet have a mechanism to automatically rename deprecated keys // for the time being, we'll do it manually: - const sess = config.session; - const session = { - idleTimeout: (sess && sess.idleTimeout) || config.sessionTimeout || null, - lifespan: (sess && sess.lifespan) || null, - }; - + const deprecatedSessionTimeout = + typeof config.sessionTimeout === 'number' ? duration(config.sessionTimeout) : null; const val = { ...config, encryptionKey, secureCookies, - session, + session: { + ...config.session, + idleTimeout: config.session.idleTimeout || deprecatedSessionTimeout, + }, }; delete val.sessionTimeout; // DEPRECATED return val; diff --git a/x-pack/plugins/security/server/elasticsearch_client_plugin.ts b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts new file mode 100644 index 00000000000000..60d947bd658637 --- /dev/null +++ b/x-pack/plugins/security/server/elasticsearch_client_plugin.ts @@ -0,0 +1,576 @@ +/* + * 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 elasticsearchClientPlugin(Client: any, config: unknown, components: any) { + const ca = components.clientAction.factory; + + Client.prototype.shield = components.clientAction.namespaceFactory(); + const shield = Client.prototype.shield.prototype; + + /** + * Perform a [shield.authenticate](Retrieve details about the currently authenticated user) request + * + * @param {Object} params - An object with parameters used to carry out this action + */ + shield.authenticate = ca({ + params: {}, + url: { + fmt: '/_security/_authenticate', + }, + }); + + /** + * Perform a [shield.changePassword](Change the password of a user) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - The username of the user to change the password for + */ + shield.changePassword = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + urls: [ + { + fmt: '/_security/user/<%=username%>/_password', + req: { + username: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/user/_password', + }, + ], + needBody: true, + method: 'POST', + }); + + /** + * Perform a [shield.clearCachedRealms](Clears the internal user caches for specified realms) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.usernames - Comma-separated list of usernames to clear from the cache + * @param {String} params.realms - Comma-separated list of realms to clear + */ + shield.clearCachedRealms = ca({ + params: { + usernames: { + type: 'string', + required: false, + }, + }, + url: { + fmt: '/_security/realm/<%=realms%>/_clear_cache', + req: { + realms: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); + + /** + * Perform a [shield.clearCachedRoles](Clears the internal caches for specified roles) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.name - Role name + */ + shield.clearCachedRoles = ca({ + params: {}, + url: { + fmt: '/_security/role/<%=name%>/_clear_cache', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + method: 'POST', + }); + + /** + * Perform a [shield.deleteRole](Remove a role from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.name - Role name + */ + shield.deleteRole = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + method: 'DELETE', + }); + + /** + * Perform a [shield.deleteUser](Remove a user from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - username + */ + shield.deleteUser = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'string', + required: true, + }, + }, + }, + method: 'DELETE', + }); + + /** + * Perform a [shield.getRole](Retrieve one or more roles from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String} params.name - Role name + */ + shield.getRole = ca({ + params: {}, + urls: [ + { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/role', + }, + ], + }); + + /** + * Perform a [shield.getUser](Retrieve one or more users from the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {String, String[], Boolean} params.username - A comma-separated list of usernames + */ + shield.getUser = ca({ + params: {}, + urls: [ + { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'list', + required: false, + }, + }, + }, + { + fmt: '/_security/user', + }, + ], + }); + + /** + * Perform a [shield.putRole](Update or create a role for the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.name - Role name + */ + shield.putRole = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/role/<%=name%>', + req: { + name: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + + /** + * Perform a [shield.putUser](Update or create a user for the native shield realm) request + * + * @param {Object} params - An object with parameters used to carry out this action + * @param {Boolean} params.refresh - Refresh the index after performing the operation + * @param {String} params.username - The username of the User + */ + shield.putUser = ca({ + params: { + refresh: { + type: 'boolean', + }, + }, + url: { + fmt: '/_security/user/<%=username%>', + req: { + username: { + type: 'string', + required: true, + }, + }, + }, + needBody: true, + method: 'PUT', + }); + + /** + * Perform a [shield.getUserPrivileges](Retrieve a user's list of privileges) request + * + */ + shield.getUserPrivileges = ca({ + params: {}, + urls: [ + { + fmt: '/_security/user/_privileges', + }, + ], + }); + + /** + * Asks Elasticsearch to prepare SAML authentication request to be sent to + * the 3rd-party SAML identity provider. + * + * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL + * in the Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch + * will choose the right SAML realm. + * + * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. + * + * @returns {{realm: string, id: string, redirect: string}} Object that includes identifier + * of the SAML realm used to prepare authentication request, encrypted request token to be + * sent to Elasticsearch with SAML response and redirect URL to the identity provider that + * will be used to authenticate user. + */ + shield.samlPrepare = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/prepare', + }, + }); + + /** + * Sends SAML response returned by identity provider to Elasticsearch for validation. + * + * @param {Array.} ids A list of encrypted request tokens returned within SAML + * preparation response. + * @param {string} content SAML response returned by identity provider. + * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm + * that should be used to authenticate request. + * + * @returns {{username: string, access_token: string, expires_in: number}} Object that + * includes name of the user, access token to use for any consequent requests that + * need to be authenticated and a number of seconds after which access token will expire. + */ + shield.samlAuthenticate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/authenticate', + }, + }); + + /** + * Invalidates SAML access token. + * + * @param {string} token SAML access token that needs to be invalidated. + * + * @returns {{redirect?: string}} + */ + shield.samlLogout = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/logout', + }, + }); + + /** + * Invalidates SAML session based on Logout Request received from the Identity Provider. + * + * @param {string} queryString URL encoded query string provided by Identity Provider. + * @param {string} [acs] Optional assertion consumer service URL to use for SAML request or URL in the + * Kibana to which identity provider will post SAML response. Based on the ACS Elasticsearch + * will choose the right SAML realm to invalidate session. + * @param {string} [realm] Optional name of the Elasticsearch SAML realm to use to handle request. + * + * @returns {{redirect?: string}} + */ + shield.samlInvalidate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/saml/invalidate', + }, + }); + + /** + * Asks Elasticsearch to prepare an OpenID Connect authentication request to be sent to + * the 3rd-party OpenID Connect provider. + * + * @param {string} realm The OpenID Connect realm name in Elasticsearch + * + * @returns {{state: string, nonce: string, redirect: string}} Object that includes two opaque parameters that need + * to be sent to Elasticsearch with the OpenID Connect response and redirect URL to the OpenID Connect provider that + * will be used to authenticate user. + */ + shield.oidcPrepare = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/prepare', + }, + }); + + /** + * Sends the URL to which the OpenID Connect Provider redirected the UA to Elasticsearch for validation. + * + * @param {string} state The state parameter that was returned by Elasticsearch in the + * preparation response. + * @param {string} nonce The nonce parameter that was returned by Elasticsearch in the + * preparation response. + * @param {string} redirect_uri The URL to where the UA was redirected by the OpenID Connect provider. + * @param {string} [realm] Optional string used to identify the name of the OpenID Connect realm + * that should be used to authenticate request. + * + * @returns {{username: string, access_token: string, refresh_token; string, expires_in: number}} Object that + * includes name of the user, access token to use for any consequent requests that + * need to be authenticated and a number of seconds after which access token will expire. + */ + shield.oidcAuthenticate = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/authenticate', + }, + }); + + /** + * Invalidates an access token and refresh token pair that was generated after an OpenID Connect authentication. + * + * @param {string} token An access token that was created by authenticating to an OpenID Connect realm and + * that needs to be invalidated. + * @param {string} refresh_token A refresh token that was created by authenticating to an OpenID Connect realm and + * that needs to be invalidated. + * + * @returns {{redirect?: string}} If the Elasticsearch OpenID Connect realm configuration and the + * OpenID Connect provider supports RP-initiated SLO, a URL to redirect the UA + */ + shield.oidcLogout = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oidc/logout', + }, + }); + + /** + * Refreshes an access token. + * + * @param {string} grant_type Currently only "refresh_token" grant type is supported. + * @param {string} refresh_token One-time refresh token that will be exchanged to the new access/refresh token pair. + * + * @returns {{access_token: string, type: string, expires_in: number, refresh_token: string}} + */ + shield.getAccessToken = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/oauth2/token', + }, + }); + + /** + * Invalidates an access token. + * + * @param {string} token The access token to invalidate + * + * @returns {{created: boolean}} + */ + shield.deleteAccessToken = ca({ + method: 'DELETE', + needBody: true, + params: { + token: { + type: 'string', + }, + }, + url: { + fmt: '/_security/oauth2/token', + }, + }); + + shield.getPrivilege = ca({ + method: 'GET', + urls: [ + { + fmt: '/_security/privilege/<%=privilege%>', + req: { + privilege: { + type: 'string', + required: false, + }, + }, + }, + { + fmt: '/_security/privilege', + }, + ], + }); + + shield.deletePrivilege = ca({ + method: 'DELETE', + urls: [ + { + fmt: '/_security/privilege/<%=application%>/<%=privilege%>', + req: { + application: { + type: 'string', + required: true, + }, + privilege: { + type: 'string', + required: true, + }, + }, + }, + ], + }); + + shield.postPrivileges = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/privilege', + }, + }); + + shield.hasPrivileges = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/user/_has_privileges', + }, + }); + + shield.getBuiltinPrivileges = ca({ + params: {}, + urls: [ + { + fmt: '/_security/privilege/_builtin', + }, + ], + }); + + /** + * Gets API keys in Elasticsearch + * @param {boolean} owner A boolean flag that can be used to query API keys owned by the currently authenticated user. + * Defaults to false. The realm_name or username parameters cannot be specified when this parameter is set to true as + * they are assumed to be the currently authenticated ones. + */ + shield.getAPIKeys = ca({ + method: 'GET', + urls: [ + { + fmt: `/_security/api_key?owner=<%=owner%>`, + req: { + owner: { + type: 'boolean', + required: true, + }, + }, + }, + ], + }); + + /** + * Creates an API key in Elasticsearch for the current user. + * + * @param {string} name A name for this API key + * @param {object} role_descriptors Role descriptors for this API key, if not + * provided then permissions of authenticated user are applied. + * @param {string} [expiration] Optional expiration for the API key being generated. If expiration + * is not provided then the API keys do not expire. + * + * @returns {{id: string, name: string, api_key: string, expiration?: number}} + */ + shield.createAPIKey = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/api_key', + }, + }); + + /** + * Invalidates an API key in Elasticsearch. + * + * @param {string} [id] An API key id. + * @param {string} [name] An API key name. + * @param {string} [realm_name] The name of an authentication realm. + * @param {string} [username] The username of a user. + * + * NOTE: While all parameters are optional, at least one of them is required. + * + * @returns {{invalidated_api_keys: string[], previously_invalidated_api_keys: string[], error_count: number, error_details?: object[]}} + */ + shield.invalidateAPIKey = ca({ + method: 'DELETE', + needBody: true, + url: { + fmt: '/_security/api_key', + }, + }); + + /** + * Gets an access token in exchange to the certificate chain for the target subject distinguished name. + * + * @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not + * base64url-encoded) DER PKIX certificate values. + * + * @returns {{access_token: string, type: string, expires_in: number}} + */ + shield.delegatePKI = ca({ + method: 'POST', + needBody: true, + url: { + fmt: '/_security/delegate_pki', + }, + }); +} diff --git a/x-pack/plugins/security/server/errors.ts b/x-pack/plugins/security/server/errors.ts index e0c2918991696c..b5f3667558f557 100644 --- a/x-pack/plugins/security/server/errors.ts +++ b/x-pack/plugins/security/server/errors.ts @@ -5,11 +5,25 @@ */ import Boom from 'boom'; +import { CustomHttpResponseOptions, ResponseError } from '../../../../src/core/server'; export function wrapError(error: any) { return Boom.boomify(error, { statusCode: getErrorStatusCode(error) }); } +/** + * Wraps error into error suitable for Core's custom error response. + * @param error Any error instance. + */ +export function wrapIntoCustomErrorResponse(error: any) { + const wrappedError = wrapError(error); + return { + body: wrappedError, + headers: wrappedError.output.headers, + statusCode: wrappedError.output.statusCode, + } as CustomHttpResponseOptions; +} + /** * Extracts error code from Boom and Elasticsearch "native" errors. * @param error Error instance to extract status code from. diff --git a/x-pack/plugins/security/server/index.ts b/x-pack/plugins/security/server/index.ts index ec43bbd95901aa..e72e94e9cd94b0 100644 --- a/x-pack/plugins/security/server/index.ts +++ b/x-pack/plugins/security/server/index.ts @@ -9,18 +9,8 @@ import { ConfigSchema } from './config'; import { Plugin } 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. Ideally we should -// reduce number of such exports to zero and provide everything we want to expose via Setup/Start -// run-time contracts. -export { wrapError } from './errors'; -export { - canRedirectRequest, - AuthenticationResult, - DeauthenticationResult, - OIDCAuthenticationFlow, - CreateAPIKeyResult, -} from './authentication'; - +// functions or removal of exports should be considered as a breaking change. +export { AuthenticationResult, DeauthenticationResult, CreateAPIKeyResult } from './authentication'; export { PluginSetupContract } from './plugin'; export const config = { schema: ConfigSchema }; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 26788c3ef9230a..cce928976accc7 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -7,6 +7,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; import { IClusterClient, CoreSetup } from '../../../../src/core/server'; +import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; @@ -48,19 +49,9 @@ describe('Security Plugin', () => { Object { "__legacyCompat": Object { "config": Object { - "authc": Object { - "providers": Array [ - "saml", - "token", - ], - }, "cookieName": "sid", "loginAssistanceMessage": undefined, "secureCookies": true, - "session": Object { - "idleTimeout": 1500, - "lifespan": null, - }, }, "license": Object { "getFeatures": [Function], @@ -115,7 +106,7 @@ describe('Security Plugin', () => { expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledTimes(1); expect(mockCoreSetup.elasticsearch.createClient).toHaveBeenCalledWith('security', { - plugins: [require('../../../legacy/server/lib/esjs_shield_plugin')], + plugins: [elasticsearchClientPlugin], }); }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index cb197ecaf7e10a..a395278a5143eb 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Subscription } from 'rxjs'; +import { Subscription, combineLatest } from 'rxjs'; import { first } from 'rxjs/operators'; import { IClusterClient, @@ -28,6 +28,7 @@ import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from './licensing'; import { setupSavedObjects } from './saved_objects'; import { SecurityAuditLogger } from './audit'; +import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], @@ -42,7 +43,6 @@ export type FeaturesService = Pick; */ export interface LegacyAPI { isSystemAPIRequest: (request: KibanaRequest) => boolean; - kibanaIndexName: string; cspRules: string; savedObjects: SavedObjectsLegacyService; auditLogger: { @@ -72,12 +72,9 @@ export interface PluginSetupContract { registerPrivilegesWithCluster: () => void; license: SecurityLicense; config: RecursiveReadonly<{ - session: { - idleTimeout: number | null; - lifespan: number | null; - }; secureCookies: boolean; - authc: { providers: string[] }; + cookieName: string; + loginAssistanceMessage: string; }>; }; } @@ -121,12 +118,15 @@ export class Plugin { core: CoreSetup, { features, licensing }: PluginSetupDependencies ): Promise> { - const config = await createConfig$(this.initializerContext, core.http.isTlsEnabled) + const [config, legacyConfig] = await combineLatest([ + createConfig$(this.initializerContext, core.http.isTlsEnabled), + this.initializerContext.config.legacy.globalConfig$, + ]) .pipe(first()) .toPromise(); this.clusterClient = core.elasticsearch.createClient('security', { - plugins: [require('../../../legacy/server/lib/esjs_shield_plugin')], + plugins: [elasticsearchClientPlugin], }); const { license, update: updateLicense } = new SecurityLicenseService().setup(); @@ -148,7 +148,7 @@ export class Plugin { clusterClient: this.clusterClient, license, loggers: this.initializerContext.logger, - getLegacyAPI: this.getLegacyAPI, + kibanaIndexName: legacyConfig.kibana.index, packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, featuresService: features, @@ -205,13 +205,8 @@ export class Plugin { // exception may be `sessionTimeout` as other parts of the app may want to know it. config: { loginAssistanceMessage: config.loginAssistanceMessage, - session: { - idleTimeout: config.session.idleTimeout, - lifespan: config.session.lifespan, - }, secureCookies: config.secureCookies, cookieName: config.cookieName, - authc: { providers: config.authc.providers }, }, }, }); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.test.ts b/x-pack/plugins/security/server/routes/api_keys/get.test.ts new file mode 100644 index 00000000000000..2b2283edea2e83 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/get.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { defineGetApiKeysRoutes } from './get'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; + +interface TestOptions { + isAdmin?: boolean; + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('Get API keys', () => { + const getApiKeysTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponse, + asserts, + isAdmin = true, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + if (apiResponse) { + mockScopedClusterClient.callAsCurrentUser.mockImplementation(apiResponse); + } + + defineGetApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key', + query: { isAdmin: isAdmin.toString() }, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith(mockRequest); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.getAPIKeys', + { owner: !isAdmin } + ); + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + getApiKeysTest('returns API keys', { + apiResponse: async () => ({ + api_keys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + asserts: { + statusCode: 200, + result: { + apiKeys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }, + }, + }); + getApiKeysTest('returns only valid API keys', { + apiResponse: async () => ({ + api_keys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key1', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: true, + username: 'elastic', + realm: 'reserved', + }, + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + asserts: { + statusCode: 200, + result: { + apiKeys: [ + { + id: 'YCLV7m0BJ3xI4hhWB648', + name: 'test-api-key2', + creation: 1571670001452, + expiration: 1571756401452, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/get.ts b/x-pack/plugins/security/server/routes/api_keys/get.ts new file mode 100644 index 00000000000000..6e98b4b098405b --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/get.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { ApiKey } from '../../../common/model'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineGetApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key', + validate: { + query: schema.object({ + // We don't use `schema.boolean` here, because all query string parameters are treated as + // strings and @kbn/config-schema doesn't coerce strings to booleans. + // + // A boolean flag that can be used to query API keys owned by the currently authenticated + // user. `false` means that only API keys of currently authenticated user will be returned. + isAdmin: schema.oneOf([schema.literal('true'), schema.literal('false')]), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const isAdmin = request.query.isAdmin === 'true'; + const { api_keys: apiKeys } = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getAPIKeys', { owner: !isAdmin })) as { api_keys: ApiKey[] }; + + const validKeys = apiKeys.filter(({ invalidated }) => !invalidated); + + return response.ok({ body: { apiKeys: validKeys } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts new file mode 100644 index 00000000000000..d75eb1bcbe9614 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { defineGetApiKeysRoutes } from './get'; +import { defineCheckPrivilegesRoutes } from './privileges'; +import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { RouteDefinitionParams } from '..'; + +export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineGetApiKeysRoutes(params); + defineCheckPrivilegesRoutes(params); + defineInvalidateApiKeysRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts new file mode 100644 index 00000000000000..4ea21bda5f743b --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.test.ts @@ -0,0 +1,220 @@ +/* + * 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 Boom from 'boom'; +import { Type } from '@kbn/config-schema'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { defineInvalidateApiKeysRoutes } from './invalidate'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponses?: Array<() => Promise>; + payload?: Record; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +describe('Invalidate API keys', () => { + const postInvalidateTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponses = [], + asserts, + payload, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); + const [[{ validate }, handler]] = mockRouteDefinitionParams.router.post.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'post', + path: '/internal/security/api_key/invalidate', + body: payload !== undefined ? (validate as any).body.validate(payload) : undefined, + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( + mockRequest + ); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('request validation', () => { + let requestBodySchema: Type; + beforeEach(() => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + defineInvalidateApiKeysRoutes(mockRouteDefinitionParams); + + const [[{ validate }]] = mockRouteDefinitionParams.router.post.mock.calls; + requestBodySchema = (validate as any).body; + }); + + test('requires both isAdmin and apiKeys parameters', () => { + expect(() => + requestBodySchema.validate({}, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys]: expected value of type [array] but got [undefined]"` + ); + + expect(() => + requestBodySchema.validate({ apiKeys: [] }, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.isAdmin]: expected value of type [boolean] but got [undefined]"` + ); + + expect(() => + requestBodySchema.validate({ apiKeys: {}, isAdmin: true }, {}, 'request body') + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys]: expected value of type [array] but got [Object]"` + ); + + expect(() => + requestBodySchema.validate( + { + apiKeys: [{ id: 'some-id', name: 'some-name', unknown: 'some-unknown' }], + isAdmin: true, + }, + {}, + 'request body' + ) + ).toThrowErrorMatchingInlineSnapshot( + `"[request body.apiKeys.0.unknown]: definition for this key is missing"` + ); + }); + }); + + describe('failure', () => { + postInvalidateTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + payload: { apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], isAdmin: true }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + postInvalidateTest('returns error from cluster client', { + apiResponses: [ + async () => { + throw error; + }, + ], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: true, + }, + asserts: { + apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + statusCode: 200, + result: { + itemsInvalidated: [], + errors: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + error: Boom.notAcceptable('test not acceptable message'), + }, + ], + }, + }, + }); + }); + + describe('success', () => { + postInvalidateTest('invalidates API keys', { + apiResponses: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: true, + }, + asserts: { + apiArguments: [['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }]], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + errors: [], + }, + }, + }); + + postInvalidateTest('adds "owner" to body if isAdmin=false', { + apiResponses: [async () => null], + payload: { + apiKeys: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + isAdmin: false, + }, + asserts: { + apiArguments: [ + ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV', owner: true } }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key' }], + errors: [], + }, + }, + }); + + postInvalidateTest('returns only successful invalidation requests', { + apiResponses: [ + async () => null, + async () => { + throw Boom.notAcceptable('test not acceptable message'); + }, + ], + payload: { + apiKeys: [ + { id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }, + { id: 'ab8If24B1bKsmSLTAhNC', name: 'my-api-key2' }, + ], + isAdmin: true, + }, + asserts: { + apiArguments: [ + ['shield.invalidateAPIKey', { body: { id: 'si8If24B1bKsmSLTAhJV' } }], + ['shield.invalidateAPIKey', { body: { id: 'ab8If24B1bKsmSLTAhNC' } }], + ], + statusCode: 200, + result: { + itemsInvalidated: [{ id: 'si8If24B1bKsmSLTAhJV', name: 'my-api-key1' }], + errors: [ + { + id: 'ab8If24B1bKsmSLTAhNC', + name: 'my-api-key2', + error: Boom.notAcceptable('test not acceptable message'), + }, + ], + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/invalidate.ts b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts new file mode 100644 index 00000000000000..cb86c1024ae9a0 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/invalidate.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { ApiKey } from '../../../common/model'; +import { wrapError, wrapIntoCustomErrorResponse } from '../../errors'; +import { RouteDefinitionParams } from '..'; + +interface ResponseType { + itemsInvalidated: Array>; + errors: Array & { error: Error }>; +} + +export function defineInvalidateApiKeysRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/api_key/invalidate', + validate: { + body: schema.object({ + apiKeys: schema.arrayOf(schema.object({ id: schema.string(), name: schema.string() })), + isAdmin: schema.boolean(), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const scopedClusterClient = clusterClient.asScoped(request); + + // Invalidate all API keys in parallel. + const invalidationResult = ( + await Promise.all( + request.body.apiKeys.map(async key => { + try { + const body: { id: string; owner?: boolean } = { id: key.id }; + if (!request.body.isAdmin) { + body.owner = true; + } + + // Send the request to invalidate the API key and return an error if it could not be deleted. + await scopedClusterClient.callAsCurrentUser('shield.invalidateAPIKey', { body }); + return { key, error: undefined }; + } catch (error) { + return { key, error: wrapError(error) }; + } + }) + ) + ).reduce( + (responseBody, { key, error }) => { + if (error) { + responseBody.errors.push({ ...key, error }); + } else { + responseBody.itemsInvalidated.push(key); + } + return responseBody; + }, + { itemsInvalidated: [], errors: [] } as ResponseType + ); + + return response.ok({ body: invalidationResult }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts new file mode 100644 index 00000000000000..866e455063bdc0 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -0,0 +1,187 @@ +/* + * 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 Boom from 'boom'; +import { LICENSE_CHECK_STATE, LicenseCheck } from '../../../../licensing/server'; +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; + +import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import { defineCheckPrivilegesRoutes } from './privileges'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponses?: Array<() => Promise>; + asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; +} + +describe('Check API keys privileges', () => { + const getPrivilegesTest = ( + description: string, + { + licenseCheckResult = { state: LICENSE_CHECK_STATE.Valid }, + apiResponses = [], + asserts, + }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + for (const apiResponse of apiResponses) { + mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); + } + + defineCheckPrivilegesRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/privileges', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (Array.isArray(asserts.apiArguments)) { + for (const apiArguments of asserts.apiArguments) { + expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( + mockRequest + ); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(...apiArguments); + } + } else { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + getPrivilegesTest('returns result of license checker', { + licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + getPrivilegesTest('returns error from cluster client', { + apiResponses: [ + async () => { + throw error; + }, + async () => {}, + ], + asserts: { + apiArguments: [ + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ['shield.getAPIKeys', { owner: true }], + ], + statusCode: 406, + result: error, + }, + }); + }); + + describe('success', () => { + getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {}, + }), + async () => ({ + api_keys: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: true }, + }, + }); + + getPrivilegesTest( + 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: true, manage_security: true }, + index: {}, + application: {}, + }), + async () => { + throw Boom.unauthorized('api keys are not enabled'); + }, + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: false, isAdmin: true }, + }, + } + ); + + getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { + apiResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false }, + index: {}, + application: {}, + }), + async () => ({ + api_keys: [ + { + id: 'si8If24B1bKsmSLTAhJV', + name: 'my-api-key', + creation: 1574089261632, + expiration: 1574175661632, + invalidated: false, + username: 'elastic', + realm: 'reserved', + }, + ], + }), + ], + asserts: { + apiArguments: [ + ['shield.getAPIKeys', { owner: true }], + ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts new file mode 100644 index 00000000000000..216d1ef1bf4a44 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.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 { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/privileges', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const scopedClusterClient = clusterClient.asScoped(request); + + const [ + { + cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + }, + { areApiKeysEnabled }, + ] = await Promise.all([ + scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { + body: { cluster: ['manage_security', 'manage_api_key'] }, + }), + scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( + // If the API returns a truthy result that means it's enabled. + result => ({ areApiKeysEnabled: !!result }), + // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. + e => + e.message.includes('api keys are not enabled') + ? Promise.resolve({ areApiKeysEnabled: false }) + : Promise.reject(e) + ), + ]); + + return response.ok({ + body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authentication/basic.test.ts b/x-pack/plugins/security/server/routes/authentication/basic.test.ts new file mode 100644 index 00000000000000..8e24f99b1302d4 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/basic.test.ts @@ -0,0 +1,172 @@ +/* + * 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 { Type } from '@kbn/config-schema'; +import { + IRouter, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, AuthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineBasicRoutes } from './basic'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authenticationMock } from '../../authentication/index.mock'; +import { authorizationMock } from '../../authorization/index.mock'; + +describe('Basic authentication routes', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockContext: RequestHandlerContext; + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineBasicRoutes({ + router, + clusterClient: elasticsearchServiceMock.createClusterClient(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + }); + + describe('login', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/security/login' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.validate).toEqual({ + body: expect.any(Type), + query: undefined, + params: undefined, + }); + + const bodyValidator = (routeConfig.validate as any).body as Type; + expect(bodyValidator.validate({ username: 'user', password: 'password' })).toEqual({ + username: 'user', + password: 'password', + }); + + expect(() => bodyValidator.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => bodyValidator.validate({ username: 'user' })).toThrowErrorMatchingInlineSnapshot( + `"[password]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodyValidator.validate({ password: 'password' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodyValidator.validate({ username: '', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodyValidator.validate({ username: 'user', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[password]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodyValidator.validate({ username: '', password: 'password' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + }); + + it('returns 500 if authentication throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.login.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + it('returns 401 if authentication fails.', async () => { + const failureReason = new Error('Something went wrong.'); + authc.login.mockResolvedValue(AuthenticationResult.failed(failureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual(failureReason); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + it('returns 401 if authentication is not handled.', async () => { + authc.login.mockResolvedValue(AuthenticationResult.notHandled()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual('Unauthorized'); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + + describe('authentication succeeds', () => { + it(`returns user data`, async () => { + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).toHaveBeenCalledWith(mockRequest, { + provider: 'basic', + value: { username: 'user', password: 'password' }, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authentication/basic.ts b/x-pack/plugins/security/server/routes/authentication/basic.ts new file mode 100644 index 00000000000000..453dc1c4ea3b55 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/basic.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 { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for Basic/Token authentication. + */ +export function defineBasicRoutes({ router, authc, config }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/login', + validate: { + body: schema.object({ + username: schema.string({ minLength: 1 }), + password: schema.string({ minLength: 1 }), + }), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const { username, password } = request.body; + + try { + // We should prefer `token` over `basic` if possible. + const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password }, + }); + + if (!authenticationResult.succeeded()) { + return response.unauthorized({ body: authenticationResult.error }); + } + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts new file mode 100644 index 00000000000000..f57fb1d5a7d668 --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Type } from '@kbn/config-schema'; +import { + IRouter, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, DeauthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineCommonRoutes } from './common'; + +import { + elasticsearchServiceMock, + httpServerMock, + httpServiceMock, + loggingServiceMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authenticationMock } from '../../authentication/index.mock'; +import { authorizationMock } from '../../authorization/index.mock'; + +describe('Common authentication routes', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockContext: RequestHandlerContext; + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineCommonRoutes({ + router, + clusterClient: elasticsearchServiceMock.createClusterClient(), + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + }); + + describe('logout', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/api/security/logout' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + expect(routeConfig.validate).toEqual({ + body: undefined, + query: expect.any(Type), + params: undefined, + }); + + const queryValidator = (routeConfig.validate as any).query as Type; + expect(queryValidator.validate({ someRandomField: 'some-random' })).toEqual({ + someRandomField: 'some-random', + }); + expect(queryValidator.validate({})).toEqual({}); + expect(queryValidator.validate(undefined)).toEqual({}); + }); + + it('returns 500 if deauthentication throws unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.logout.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('returns 500 if authenticator fails to logout.', async () => { + const failureReason = new Error('Something went wrong.'); + authc.logout.mockResolvedValue(DeauthenticationResult.failed(failureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('returns 400 for AJAX requests that can not handle redirect.', async () => { + const mockAjaxRequest = httpServerMock.createKibanaRequest({ + headers: { 'kbn-xsrf': 'xsrf' }, + }); + + const response = await routeHandler(mockContext, mockAjaxRequest, kibanaResponseFactory); + + expect(response.status).toBe(400); + expect(response.payload).toEqual('Client should be able to process redirect response.'); + expect(authc.logout).not.toHaveBeenCalled(); + }); + + it('redirects user to the URL returned by authenticator.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.redirectTo('https://custom.logout')); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: 'https://custom.logout' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('redirects user to the base path if deauthentication succeeds.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.succeeded()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: '/mock-server-basepath/' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + + it('redirects user to the base path if deauthentication is not handled.', async () => { + authc.logout.mockResolvedValue(DeauthenticationResult.notHandled()); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(302); + expect(response.payload).toBeUndefined(); + expect(response.options).toEqual({ headers: { location: '/mock-server-basepath/' } }); + expect(authc.logout).toHaveBeenCalledWith(mockRequest); + }); + }); + + describe('me', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { username: 'user', password: 'password' }, + }); + + beforeEach(() => { + const [loginRouteConfig, loginRouteHandler] = router.get.mock.calls.find( + ([{ path }]) => path === '/internal/security/me' + )!; + + routeConfig = loginRouteConfig; + routeHandler = loginRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.options).toBeUndefined(); + expect(routeConfig.validate).toBe(false); + }); + + it('returns 500 if cannot retrieve current user due to unhandled exception.', async () => { + const unhandledException = new Error('Something went wrong.'); + authc.getCurrentUser.mockRejectedValue(unhandledException); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(unhandledException); + expect(authc.getCurrentUser).toHaveBeenCalledWith(mockRequest); + }); + + it('returns current user.', async () => { + const mockUser = mockAuthenticatedUser(); + authc.getCurrentUser.mockResolvedValue(mockUser); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toEqual(mockUser); + expect(authc.getCurrentUser).toHaveBeenCalledWith(mockRequest); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts new file mode 100644 index 00000000000000..cb4ec196459eee --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { canRedirectRequest } from '../../authentication'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes that are common to various authentication mechanisms. + */ +export function defineCommonRoutes({ router, authc, basePath, logger }: RouteDefinitionParams) { + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/logout', '/api/security/v1/logout']) { + router.get( + { + path, + // Allow unknown query parameters as this endpoint can be hit by the 3rd-party with any + // set of query string parameters (e.g. SAML/OIDC logout request parameters). + validate: { query: schema.object({}, { allowUnknowns: true }) }, + options: { authRequired: false }, + }, + async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/logout') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/logout" URL instead.`, + { tags: ['deprecation'] } + ); + } + + if (!canRedirectRequest(request)) { + return response.badRequest({ + body: 'Client should be able to process redirect response.', + }); + } + + try { + const deauthenticationResult = await authc.logout(request); + if (deauthenticationResult.failed()) { + return response.customError(wrapIntoCustomErrorResponse(deauthenticationResult.error)); + } + + return response.redirected({ + headers: { location: deauthenticationResult.redirectURL || `${serverBasePath}/` }, + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + ); + } + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/internal/security/me', '/api/security/v1/me']) { + router.get( + { path, validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + if (path === '/api/security/v1/me') { + logger.warn( + `The "${basePath.serverBasePath}${path}" endpoint is deprecated and will be removed in the next major version.`, + { tags: ['deprecation'] } + ); + } + + try { + return response.ok({ body: (await authc.getCurrentUser(request)) as any }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); + } +} diff --git a/x-pack/plugins/security/server/routes/authentication/index.ts b/x-pack/plugins/security/server/routes/authentication/index.ts index 086647dcb34597..21f015cc23b68c 100644 --- a/x-pack/plugins/security/server/routes/authentication/index.ts +++ b/x-pack/plugins/security/server/routes/authentication/index.ts @@ -6,11 +6,39 @@ import { defineSessionRoutes } from './session'; import { defineSAMLRoutes } from './saml'; +import { defineBasicRoutes } from './basic'; +import { defineCommonRoutes } from './common'; +import { defineOIDCRoutes } from './oidc'; import { RouteDefinitionParams } from '..'; +export function createCustomResourceResponse(body: string, contentType: string, cspRules: string) { + return { + body, + headers: { + 'content-type': contentType, + 'cache-control': 'private, no-cache, no-store', + 'content-security-policy': cspRules, + }, + statusCode: 200, + }; +} + export function defineAuthenticationRoutes(params: RouteDefinitionParams) { defineSessionRoutes(params); + defineCommonRoutes(params); + + if ( + params.config.authc.providers.includes('basic') || + params.config.authc.providers.includes('token') + ) { + defineBasicRoutes(params); + } + if (params.config.authc.providers.includes('saml')) { defineSAMLRoutes(params); } + + if (params.config.authc.providers.includes('oidc')) { + defineOIDCRoutes(params); + } } diff --git a/x-pack/plugins/security/server/routes/authentication/oidc.ts b/x-pack/plugins/security/server/routes/authentication/oidc.ts new file mode 100644 index 00000000000000..8483630763ae6b --- /dev/null +++ b/x-pack/plugins/security/server/routes/authentication/oidc.ts @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import { KibanaRequest, KibanaResponseFactory } from '../../../../../../src/core/server'; +import { OIDCAuthenticationFlow } from '../../authentication'; +import { createCustomResourceResponse } from '.'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { ProviderLoginAttempt } from '../../authentication/providers/oidc'; +import { RouteDefinitionParams } from '..'; + +/** + * Defines routes required for SAML authentication. + */ +export function defineOIDCRoutes({ + router, + logger, + authc, + getLegacyAPI, + basePath, +}: RouteDefinitionParams) { + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc/implicit', '/api/security/v1/oidc/implicit']) { + /** + * The route should be configured as a redirect URI in OP when OpenID Connect implicit flow + * is used, so that we can extract authentication response from URL fragment and send it to + * the `/api/security/oidc` route. + */ + router.get( + { + path, + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/oidc/implicit') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/implicit" URL instead.`, + { tags: ['deprecation'] } + ); + } + return response.custom( + createCustomResourceResponse( + ` + + Kibana OpenID Connect Login + + + `, + 'text/html', + getLegacyAPI().cspRules + ) + ); + } + ); + } + + /** + * The route that accompanies `/api/security/oidc/implicit` and renders a JavaScript snippet + * that extracts fragment part from the URL and send it to the `/api/security/oidc` route. + * We need this separate endpoint because of default CSP policy that forbids inline scripts. + */ + router.get( + { + path: '/internal/security/oidc/implicit.js', + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + return response.custom( + createCustomResourceResponse( + ` + window.location.replace( + '${serverBasePath}/api/security/oidc?authenticationResponseURI=' + encodeURIComponent(window.location.href) + ); + `, + 'text/javascript', + getLegacyAPI().cspRules + ) + ); + } + ); + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc', '/api/security/v1/oidc']) { + router.get( + { + path, + validate: { + query: schema.object( + { + authenticationResponseURI: schema.maybe(schema.uri()), + code: schema.maybe(schema.string()), + error: schema.maybe(schema.string()), + error_description: schema.maybe(schema.string()), + error_uri: schema.maybe(schema.uri()), + iss: schema.maybe(schema.uri({ scheme: ['https'] })), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + state: schema.maybe(schema.string()), + }, + // The client MUST ignore unrecognized response parameters according to + // https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation and + // https://tools.ietf.org/html/rfc6749#section-4.1.2. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + + // An HTTP GET request with a query parameter named `authenticationResponseURI` that includes URL fragment OpenID + // Connect Provider sent during implicit authentication flow to the Kibana own proxy page that extracted that URL + // fragment and put it into `authenticationResponseURI` query string parameter for this endpoint. See more details + // at https://openid.net/specs/openid-connect-core-1_0.html#ImplicitFlowAuth + let loginAttempt: ProviderLoginAttempt | undefined; + if (request.query.authenticationResponseURI) { + loginAttempt = { + flow: OIDCAuthenticationFlow.Implicit, + authenticationResponseURI: request.query.authenticationResponseURI, + }; + } else if (request.query.code || request.query.error) { + if (path === '/api/security/v1/oidc') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc" URL instead.`, + { tags: ['deprecation'] } + ); + } + + // An HTTP GET request with a query parameter named `code` (or `error`) as the response to a successful (or + // failed) authentication from an OpenID Connect Provider during authorization code authentication flow. + // See more details at https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth. + loginAttempt = { + flow: OIDCAuthenticationFlow.AuthorizationCode, + // We pass the path only as we can't be sure of the full URL and Elasticsearch doesn't need it anyway. + authenticationResponseURI: request.url.path!, + }; + } else if (request.query.iss) { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`, + { tags: ['deprecation'] } + ); + // An HTTP GET request with a query parameter named `iss` as part of a 3rd party initiated authentication. + // See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + loginAttempt = { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.query.iss, + loginHint: request.query.login_hint, + }; + } + + if (!loginAttempt) { + return response.badRequest({ body: 'Unrecognized login attempt.' }); + } + + return performOIDCLogin(request, response, loginAttempt); + }) + ); + } + + // Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used. + for (const path of ['/api/security/oidc/initiate_login', '/api/security/v1/oidc']) { + /** + * An HTTP POST request with the payload parameter named `iss` as part of a 3rd party initiated authentication. + * See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + */ + router.post( + { + path, + validate: { + body: schema.object( + { + iss: schema.uri({ scheme: ['https'] }), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + }, + // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST + // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const serverBasePath = basePath.serverBasePath; + if (path === '/api/security/v1/oidc') { + logger.warn( + `The "${serverBasePath}${path}" URL is deprecated and will stop working in the next major version, please use "${serverBasePath}/api/security/oidc/initiate_login" URL for Third-Party Initiated login instead.`, + { tags: ['deprecation'] } + ); + } + + return performOIDCLogin(request, response, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.body.iss, + loginHint: request.body.login_hint, + }); + }) + ); + } + + /** + * An HTTP GET request with the query string parameter named `iss` as part of a 3rd party initiated authentication. + * See more details at https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin + */ + router.get( + { + path: '/api/security/oidc/initiate_login', + validate: { + query: schema.object( + { + iss: schema.uri({ scheme: ['https'] }), + login_hint: schema.maybe(schema.string()), + target_link_uri: schema.maybe(schema.uri()), + }, + // Other parameters MAY be sent, if defined by extensions. Any parameters used that are not understood MUST + // be ignored by the Client according to https://openid.net/specs/openid-connect-core-1_0.html#ThirdPartyInitiatedLogin. + { allowUnknowns: true } + ), + }, + options: { authRequired: false }, + }, + createLicensedRouteHandler(async (context, request, response) => { + return performOIDCLogin(request, response, { + flow: OIDCAuthenticationFlow.InitiatedBy3rdParty, + iss: request.query.iss, + loginHint: request.query.login_hint, + }); + }) + ); + + async function performOIDCLogin( + request: KibanaRequest, + response: KibanaResponseFactory, + loginAttempt: ProviderLoginAttempt + ) { + try { + // We handle the fact that the user might get redirected to Kibana while already having a session + // Return an error notifying the user they are already logged in. + const authenticationResult = await authc.login(request, { + provider: 'oidc', + value: loginAttempt, + }); + + if (authenticationResult.succeeded()) { + return response.forbidden({ + body: i18n.translate('xpack.security.conflictingSessionError', { + defaultMessage: + 'Sorry, you already have an active Kibana session. ' + + 'If you want to start a new one, please logout from the existing session first.', + }), + }); + } + + if (authenticationResult.redirected()) { + return response.redirected({ + headers: { location: authenticationResult.redirectURL! }, + }); + } + + return response.unauthorized({ body: authenticationResult.error }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } +} diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 61f40e583d24ef..f724d0e7708be4 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -6,6 +6,7 @@ import { schema } from '@kbn/config-schema'; import { SAMLLoginStep } from '../../authentication'; +import { createCustomResourceResponse } from '.'; import { RouteDefinitionParams } from '..'; /** @@ -18,18 +19,6 @@ export function defineSAMLRoutes({ getLegacyAPI, basePath, }: RouteDefinitionParams) { - function createCustomResourceResponse(body: string, contentType: string) { - return { - body, - headers: { - 'content-type': contentType, - 'cache-control': 'private, no-cache, no-store', - 'content-security-policy': getLegacyAPI().cspRules, - }, - statusCode: 200, - }; - } - router.get( { path: '/api/security/saml/capture-url-fragment', @@ -46,7 +35,8 @@ export function defineSAMLRoutes({ `, - 'text/html' + 'text/html', + getLegacyAPI().cspRules ) ); } @@ -66,7 +56,8 @@ export function defineSAMLRoutes({ '${basePath.serverBasePath}/api/security/saml/start?redirectURLFragment=' + encodeURIComponent(window.location.hash) ); `, - 'text/javascript' + 'text/javascript', + getLegacyAPI().cspRules ) ); } diff --git a/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts index 10fe0cdd678118..6afbad8e83ebe7 100644 --- a/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/privileges/get.test.ts @@ -81,7 +81,7 @@ describe('GET privileges', () => { }; describe('failure', () => { - getPrivilegesTest(`returns result of routePreCheckLicense`, { + getPrivilegesTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts index 61c5747550d75a..22268245c3a447 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.test.ts @@ -73,16 +73,18 @@ describe('DELETE role', () => { }; describe('failure', () => { - deleteRoleTest(`returns result of license checker`, { + deleteRoleTest('returns result of license checker', { name: 'foo-role', licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notFound('test not found message'); - deleteRoleTest(`returns error from cluster client`, { + deleteRoleTest('returns error from cluster client', { name: 'foo-role', - apiResponse: () => Promise.reject(error), + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 404, result: error }, }); }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts index aab815fbe449ff..de966d6f2a7586 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/delete.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/delete.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefinitionParams) { router.delete( @@ -23,11 +23,7 @@ export function defineDeleteRolesRoutes({ router, clusterClient }: RouteDefiniti return response.noContent(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts index 1cfc1ae416ae47..bb9edbd17b2c8d 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.test.ts @@ -36,7 +36,7 @@ describe('GET role', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); @@ -75,15 +75,17 @@ describe('GET role', () => { }; describe('failure', () => { - getRoleTest(`returns result of license check`, { + getRoleTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notAcceptable('test not acceptable message'); - getRoleTest(`returns error from cluster client`, { + getRoleTest('returns error from cluster client', { name: 'first_role', - apiResponse: () => Promise.reject(error), + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 406, result: error }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get.ts b/x-pack/plugins/security/server/routes/authorization/roles/get.ts index be69e222dd0936..8c158bee1a15e5 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { transformElasticsearchRoleToRole } from './model'; export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { @@ -28,18 +28,14 @@ export function defineGetRolesRoutes({ router, authz, clusterClient }: RouteDefi body: transformElasticsearchRoleToRole( elasticsearchRole, request.params.name, - authz.getApplicationName() + authz.applicationName ), }); } return response.notFound(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts index 76ce6a272e2853..96f065d6c765ae 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.test.ts @@ -31,7 +31,7 @@ describe('GET all roles', () => { ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); @@ -67,14 +67,16 @@ describe('GET all roles', () => { }; describe('failure', () => { - getRolesTest(`returns result of license check`, { + getRolesTest('returns result of license checker', { licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, }); const error = Boom.notAcceptable('test not acceptable message'); - getRolesTest(`returns error from cluster client`, { - apiResponse: () => Promise.reject(error), + getRolesTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, asserts: { statusCode: 406, result: error }, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts index f5d2d51280fc42..24be6c60e4b129 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/get_all.ts @@ -6,7 +6,7 @@ import { RouteDefinitionParams } from '../..'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, transformElasticsearchRoleToRole } from './model'; export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteDefinitionParams) { @@ -22,11 +22,7 @@ export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteD return response.ok({ body: Object.entries(elasticsearchRoles) .map(([roleName, elasticsearchRole]) => - transformElasticsearchRoleToRole( - elasticsearchRole, - roleName, - authz.getApplicationName() - ) + transformElasticsearchRoleToRole(elasticsearchRole, roleName, authz.applicationName) ) .sort((roleA, roleB) => { if (roleA.name < roleB.name) { @@ -41,11 +37,7 @@ export function defineGetAllRolesRoutes({ router, authz, clusterClient }: RouteD }), }); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 31963987c2efb8..d19debe6924607 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -62,7 +62,7 @@ const putRoleTest = ( ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); - mockRouteDefinitionParams.authz.getApplicationName.mockReturnValue(application); + mockRouteDefinitionParams.authz.applicationName = application; mockRouteDefinitionParams.authz.privileges.get.mockReturnValue(privilegeMap); const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); @@ -138,7 +138,7 @@ describe('PUT role', () => { }); describe('failure', () => { - putRoleTest(`returns result of license checker`, { + putRoleTest('returns result of license checker', { name: 'foo-role', licenseCheckResult: { state: LICENSE_CHECK_STATE.Invalid, message: 'test forbidden message' }, asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index 92c940132e6605..5db83375afa965 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -7,7 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; -import { wrapError } from '../../../errors'; +import { wrapIntoCustomErrorResponse } from '../../../errors'; import { ElasticsearchRole, getPutPayloadSchema, @@ -42,7 +42,7 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi const body = transformPutPayloadToElasticsearchRole( request.body, - authz.getApplicationName(), + authz.applicationName, rawRoles[name] ? rawRoles[name].applications : [] ); @@ -52,11 +52,7 @@ export function definePutRolesRoutes({ router, authz, clusterClient }: RouteDefi return response.noContent(); } catch (error) { - const wrappedError = wrapError(error); - return response.customError({ - body: wrappedError, - statusCode: wrappedError.output.statusCode, - }); + return response.customError(wrapIntoCustomErrorResponse(error)); } }) ); diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index 73e276832f4741..756eaa76e2c2e6 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -12,6 +12,9 @@ import { LegacyAPI } from '../plugin'; import { defineAuthenticationRoutes } from './authentication'; import { defineAuthorizationRoutes } from './authorization'; +import { defineApiKeysRoutes } from './api_keys'; +import { defineIndicesRoutes } from './indices'; +import { defineUsersRoutes } from './users'; /** * Describes parameters used to define HTTP routes. @@ -30,4 +33,7 @@ export interface RouteDefinitionParams { export function defineRoutes(params: RouteDefinitionParams) { defineAuthenticationRoutes(params); defineAuthorizationRoutes(params); + defineApiKeysRoutes(params); + defineIndicesRoutes(params); + defineUsersRoutes(params); } diff --git a/x-pack/plugins/security/server/routes/indices/get_fields.ts b/x-pack/plugins/security/server/routes/indices/get_fields.ts new file mode 100644 index 00000000000000..64c3d4f7471ef0 --- /dev/null +++ b/x-pack/plugins/security/server/routes/indices/get_fields.ts @@ -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 { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; + +export function defineGetFieldsRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/fields/{query}', + validate: { params: schema.object({ query: schema.string() }) }, + }, + async (context, request, response) => { + try { + const indexMappings = (await clusterClient + .asScoped(request) + .callAsCurrentUser('indices.getFieldMapping', { + index: request.params.query, + fields: '*', + allowNoIndices: false, + includeDefaults: true, + })) as Record }>; + + // The flow is the following (see response format at https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html): + // 1. Iterate over all matched indices. + // 2. Extract all the field names from the `mappings` field of the particular index. + // 3. Collect and flatten the list of the field names. + // 4. Use `Set` to get only unique field names. + return response.ok({ + body: Array.from( + new Set( + Object.values(indexMappings) + .map(indexMapping => Object.keys(indexMapping.mappings)) + .flat() + ) + ), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + ); +} diff --git a/x-pack/plugins/security/server/routes/indices/index.ts b/x-pack/plugins/security/server/routes/indices/index.ts new file mode 100644 index 00000000000000..d6b5eccf0fadad --- /dev/null +++ b/x-pack/plugins/security/server/routes/indices/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 { defineGetFieldsRoutes } from './get_fields'; +import { RouteDefinitionParams } from '..'; + +export function defineIndicesRoutes(params: RouteDefinitionParams) { + defineGetFieldsRoutes(params); +} diff --git a/x-pack/plugins/security/server/routes/users/change_password.test.ts b/x-pack/plugins/security/server/routes/users/change_password.test.ts new file mode 100644 index 00000000000000..9f88d28bc115f1 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/change_password.test.ts @@ -0,0 +1,207 @@ +/* + * 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 { ObjectType } from '@kbn/config-schema'; +import { + IClusterClient, + IRouter, + IScopedClusterClient, + kibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteConfig, +} from '../../../../../../src/core/server'; +import { LICENSE_CHECK_STATE } from '../../../../licensing/server'; +import { Authentication, AuthenticationResult } from '../../authentication'; +import { ConfigType } from '../../config'; +import { LegacyAPI } from '../../plugin'; +import { defineChangeUserPasswordRoutes } from './change_password'; + +import { + elasticsearchServiceMock, + loggingServiceMock, + httpServiceMock, + httpServerMock, +} from '../../../../../../src/core/server/mocks'; +import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock'; +import { authorizationMock } from '../../authorization/index.mock'; +import { authenticationMock } from '../../authentication/index.mock'; + +describe('Change password', () => { + let router: jest.Mocked; + let authc: jest.Mocked; + let mockClusterClient: jest.Mocked; + let mockScopedClusterClient: jest.Mocked; + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + let mockContext: RequestHandlerContext; + + function checkPasswordChangeAPICall( + username: string, + request: ReturnType + ) { + expect(mockClusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith( + 'shield.changePassword', + { username, body: { password: 'new-password' } } + ); + } + + beforeEach(() => { + router = httpServiceMock.createRouter(); + authc = authenticationMock.create(); + + authc.getCurrentUser.mockResolvedValue(mockAuthenticatedUser({ username: 'user' })); + authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser())); + + mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockClusterClient = elasticsearchServiceMock.createClusterClient(); + mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + + mockContext = ({ + licensing: { + license: { check: jest.fn().mockReturnValue({ check: LICENSE_CHECK_STATE.Valid }) }, + }, + } as unknown) as RequestHandlerContext; + + defineChangeUserPasswordRoutes({ + router, + clusterClient: mockClusterClient, + basePath: httpServiceMock.createBasePath(), + logger: loggingServiceMock.create().get(), + config: { authc: { providers: ['saml'] } } as ConfigType, + authc, + authz: authorizationMock.create(), + getLegacyAPI: () => ({ cspRules: 'test-csp-rule' } as LegacyAPI), + }); + + const [changePasswordRouteConfig, changePasswordRouteHandler] = router.post.mock.calls[0]; + routeConfig = changePasswordRouteConfig; + routeHandler = changePasswordRouteHandler; + }); + + it('correctly defines route.', async () => { + expect(routeConfig.path).toBe('/internal/security/users/{username}/password'); + + const paramsSchema = (routeConfig.validate as any).params as ObjectType; + expect(() => paramsSchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[username]: expected value of type [string] but got [undefined]"` + ); + expect(() => paramsSchema.validate({ username: '' })).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + paramsSchema.validate({ username: 'a'.repeat(1025) }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value is [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa] but it must have a maximum length of [1024]."` + ); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[newPassword]: expected value of type [string] but got [undefined]"` + ); + expect(() => bodySchema.validate({ newPassword: '' })).toThrowErrorMatchingInlineSnapshot( + `"[newPassword]: value is [] but it must have a minimum length of [1]."` + ); + expect(() => + bodySchema.validate({ newPassword: '123456', password: '' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[password]: value is [] but it must have a minimum length of [1]."` + ); + }); + + describe('own password', () => { + const username = 'user'; + const mockRequest = httpServerMock.createKibanaRequest({ + params: { username }, + body: { password: 'old-password', newPassword: 'new-password' }, + }); + + it('returns 403 if old password is wrong.', async () => { + const loginFailureReason = new Error('Something went wrong.'); + authc.login.mockResolvedValue(AuthenticationResult.failed(loginFailureReason)); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(403); + expect(response.payload).toEqual(loginFailureReason); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + }); + + it(`returns 401 if user can't authenticate with new password.`, async () => { + const loginFailureReason = new Error('Something went wrong.'); + authc.login.mockImplementation(async (request, attempt) => { + const credentials = attempt.value as { username: string; password: string }; + if (credentials.username === 'user' && credentials.password === 'new-password') { + return AuthenticationResult.failed(loginFailureReason); + } + + return AuthenticationResult.succeeded(mockAuthenticatedUser()); + }); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(401); + expect(response.payload).toEqual(loginFailureReason); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('returns 500 if password update request fails.', async () => { + const failureReason = new Error('Request failed.'); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('successfully changes own password if provided old password is correct.', async () => { + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + }); + + describe('other user password', () => { + const username = 'target-user'; + const mockRequest = httpServerMock.createKibanaRequest({ + params: { username }, + body: { newPassword: 'new-password' }, + }); + + it('returns 500 if password update request fails.', async () => { + const failureReason = new Error('Request failed.'); + mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason); + + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(500); + expect(response.payload).toEqual(failureReason); + expect(authc.login).not.toHaveBeenCalled(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + + it('successfully changes user password.', async () => { + const response = await routeHandler(mockContext, mockRequest, kibanaResponseFactory); + + expect(response.status).toBe(204); + expect(response.payload).toBeUndefined(); + expect(authc.login).not.toHaveBeenCalled(); + + checkPasswordChangeAPICall(username, mockRequest); + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/users/change_password.ts b/x-pack/plugins/security/server/routes/users/change_password.ts new file mode 100644 index 00000000000000..b9d04b4bd1e0e5 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/change_password.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineChangeUserPasswordRoutes({ + authc, + router, + clusterClient, + config, +}: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/password', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + body: schema.object({ + password: schema.maybe(schema.string({ minLength: 1 })), + newPassword: schema.string({ minLength: 1 }), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + const username = request.params.username; + const { password, newPassword } = request.body; + const isCurrentUser = username === (await authc.getCurrentUser(request))!.username; + + // We should prefer `token` over `basic` if possible. + const providerToLoginWith = config.authc.providers.includes('token') ? 'token' : 'basic'; + + // If user tries to change own password, let's check if old password is valid first by trying + // to login. + if (isCurrentUser) { + try { + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password }, + // We shouldn't alter authentication state just yet. + stateless: true, + }); + + if (!authenticationResult.succeeded()) { + return response.forbidden({ body: authenticationResult.error }); + } + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + } + + try { + await clusterClient.asScoped(request).callAsCurrentUser('shield.changePassword', { + username, + body: { password: newPassword }, + }); + + // Now we authenticate user with the new password again updating current session if any. + if (isCurrentUser) { + const authenticationResult = await authc.login(request, { + provider: providerToLoginWith, + value: { username, password: newPassword }, + }); + + if (!authenticationResult.succeeded()) { + return response.unauthorized({ body: authenticationResult.error }); + } + } + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts new file mode 100644 index 00000000000000..5a3e50bb11d5c2 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -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 { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineCreateOrUpdateUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + body: schema.object({ + username: schema.string({ minLength: 1, maxLength: 1024 }), + password: schema.maybe(schema.string({ minLength: 1 })), + roles: schema.arrayOf(schema.string({ minLength: 1 })), + full_name: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + email: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), + enabled: schema.boolean({ defaultValue: true }), + }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await clusterClient.asScoped(request).callAsCurrentUser('shield.putUser', { + username: request.params.username, + // Omit `username`, `enabled` and all fields with `null` value. + body: Object.fromEntries( + Object.entries(request.body).filter( + ([key, value]) => value !== null && key !== 'enabled' && key !== 'username' + ) + ), + }); + + return response.ok({ body: request.body }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/delete.ts b/x-pack/plugins/security/server/routes/users/delete.ts new file mode 100644 index 00000000000000..99a8d5c18ab3dd --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/delete.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineDeleteUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.delete( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.deleteUser', { username: request.params.username }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/get.ts b/x-pack/plugins/security/server/routes/users/get.ts new file mode 100644 index 00000000000000..08679103725465 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/get.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineGetUserRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/users/{username}', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const username = request.params.username; + const users = (await clusterClient + .asScoped(request) + .callAsCurrentUser('shield.getUser', { username })) as Record; + + if (!users[username]) { + return response.notFound(); + } + + return response.ok({ body: users[username] }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/get_all.ts b/x-pack/plugins/security/server/routes/users/get_all.ts new file mode 100644 index 00000000000000..492ab27ab27adc --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/get_all.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. + */ + +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineGetAllUsersRoutes({ router, clusterClient }: RouteDefinitionParams) { + router.get( + { path: '/internal/security/users', validate: false }, + createLicensedRouteHandler(async (context, request, response) => { + try { + return response.ok({ + // Return only values since keys (user names) are already duplicated there. + body: Object.values( + await clusterClient.asScoped(request).callAsCurrentUser('shield.getUser') + ), + }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/index.ts b/x-pack/plugins/security/server/routes/users/index.ts new file mode 100644 index 00000000000000..931af0734b4164 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { RouteDefinitionParams } from '../index'; +import { defineGetUserRoutes } from './get'; +import { defineGetAllUsersRoutes } from './get_all'; +import { defineCreateOrUpdateUserRoutes } from './create_or_update'; +import { defineDeleteUserRoutes } from './delete'; +import { defineChangeUserPasswordRoutes } from './change_password'; + +export function defineUsersRoutes(params: RouteDefinitionParams) { + defineGetUserRoutes(params); + defineGetAllUsersRoutes(params); + defineCreateOrUpdateUserRoutes(params); + defineDeleteUserRoutes(params); + defineChangeUserPasswordRoutes(params); +} diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 21d6c840fb0179..18f7575ff75d68 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -16,6 +16,7 @@ import { Plugin } from './plugin'; // end public contract exports export { SpacesPluginSetup } from './plugin'; +export { SpacesServiceSetup } from './spaces_service'; export const config = { schema: ConfigSchema }; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index 732731ac35ecd8..0c901dc950d866 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -17,7 +17,7 @@ import { import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { PluginSetupContract as SecurityPluginSetup } from '../../security/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; import { createDefaultSpace } from './lib/create_default_space'; // @ts-ignore import { AuditLogger } from '../../../../server/lib/audit_logger'; @@ -26,7 +26,7 @@ import { SpacesAuditLogger } from './lib/audit_logger'; import { createSpacesTutorialContextFactory } from './lib/spaces_tutorial_context_factory'; import { registerSpacesUsageCollector } from './lib/spaces_usage_collector'; import { SpacesService } from './spaces_service'; -import { SpacesServiceSetup } from './spaces_service/spaces_service'; +import { SpacesServiceSetup } from './spaces_service'; import { ConfigType } from './config'; import { toggleUICapabilities } from './lib/toggle_ui_capabilities'; import { initSpacesRequestInterceptors } from './lib/request_interceptors'; diff --git a/x-pack/plugins/spaces/server/spaces_service/index.ts b/x-pack/plugins/spaces/server/spaces_service/index.ts index e37d1db3f85bb1..69a7e171a51860 100644 --- a/x-pack/plugins/spaces/server/spaces_service/index.ts +++ b/x-pack/plugins/spaces/server/spaces_service/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SpacesService } from './spaces_service'; +export { SpacesService, SpacesServiceSetup } from './spaces_service'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 55147c7863d1f8..fce656dd42e8a9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -463,9 +463,6 @@ "common.ui.management.breadcrumb": "管理", "common.ui.management.connectDataDisplayName": "データに接続", "common.ui.management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", - "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", - "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "common.ui.management.nav.menu": "管理メニュー", "common.ui.modals.cancelButtonLabel": "キャンセル", "common.ui.notify.fatalError.errorStatusMessage": "エラー {errStatus} {errStatusText}: {errMessage}", @@ -566,6 +563,9 @@ "common.ui.aggTypes.scaleMetricsLabel": "メトリック値のスケーリング (廃止)", "common.ui.aggTypes.scaleMetricsTooltip": "これを有効にすると、手動最低間隔を選択し、広い間隔が使用された場合、カウントと合計メトリックが手動で選択された間隔にスケーリングされます。", "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", + "management.editIndexPattern.createIndex.defaultButtonDescription": "すべてのデータに完全集約を実行", + "management.editIndexPattern.createIndex.defaultButtonText": "標準インデックスパターン", + "management.editIndexPattern.createIndex.defaultTypeName": "インデックスパターン", "core.ui.overlays.banner.attentionTitle": "注意", "core.ui.overlays.banner.closeButtonLabel": "閉じる", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "ホームページに移動", @@ -5170,10 +5170,8 @@ "xpack.graph.savedWorkspaces.graphWorkspacesLabel": "グラフワークスペース", "xpack.graph.saveWorkspace.successNotification.noDataSavedText": "構成が保存されましたが、データは保存されませんでした", "xpack.graph.saveWorkspace.successNotificationTitle": "「{workspaceTitle}」が保存されました", - "xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "グラフを利用できません。ライセンスが期限切れです。", "xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "グラフを利用できません", "xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "グラフを利用できません。現在ライセンス情報が利用できません。", - "xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "現在の {licenseType} ライセンスではグラフを利用できません。ライセンスをアップグレードしてください。", "xpack.graph.settings.advancedSettings.certaintyInputHelpText": "関連用語が登録される前に証拠として必要なドキュメントの最低数です", "xpack.graph.settings.advancedSettings.certaintyInputLabel": "確実性", "xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "ドキュメントのサンプルが 1 種類に偏らないように、バイアスの原因の認識に役立つフィールドを選択してください。", @@ -5997,10 +5995,8 @@ "xpack.infra.metricsExplorer.openInTSVB": "ビジュアライザーで開く", "xpack.infra.metricsExplorer.viewNodeDetail": "{name} のメトリックを表示", "xpack.infra.node.ariaLabel": "{nodeName}、クリックしてメニューを開きます", - "xpack.infra.nodeContextMenu.viewAPMTraces": "{nodeType} APM トレースを表示", "xpack.infra.nodeContextMenu.viewLogsName": "ログを表示", "xpack.infra.nodeContextMenu.viewMetricsName": "メトリックを表示", - "xpack.infra.nodeContextMenu.viewUptimeLink": "アップタイムで {nodeType} を表示", "xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "すべて", "xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "すべて", "xpack.infra.notFoundPage.noContentFoundErrorTitle": "コンテンツがありません", @@ -6058,7 +6054,6 @@ "xpack.infra.waffle.metricOptions.outboundTrafficText": "送信トラフィック", "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", - "xpack.infra.waffle.nodeTypeSwitcher.hostsLabel": "すべてのホスト", "xpack.infra.waffle.selectTwoGroupingsTitle": "最大 2 つのグループ分けを選択してください", "xpack.infra.waffle.unableToSelectGroupErrorMessage": "{nodeType} のオプションでグループを選択できません", "xpack.infra.waffle.unableToSelectMetricErrorTitle": "メトリックのオプションまたは値を選択できません", @@ -6433,7 +6428,6 @@ "xpack.maps.layerControl.tocEntry.hideDetailsButtonTitle": "レイヤー詳細を非表示", "xpack.maps.layerControl.tocEntry.showDetailsButtonAriaLabel": "レイヤー詳細を表示", "xpack.maps.layerControl.tocEntry.showDetailsButtonTitle": "レイヤー詳細を表示", - "xpack.maps.layerPanel.applyGlobalQueryCheckboxLabel": "レイヤーにグローバルフィルターを適用", "xpack.maps.layerPanel.filterEditor.addFilterButtonLabel": "フィルターを追加します", "xpack.maps.layerPanel.filterEditor.editFilterButtonLabel": "フィルターを編集", "xpack.maps.layerPanel.filterEditor.emptyState.description": "フィルターを追加してレイヤーデータを絞ります。", @@ -6468,7 +6462,6 @@ "xpack.maps.layerPanel.settingsPanel.unableToLoadTitle": "レイヤーを読み込めません", "xpack.maps.layerPanel.settingsPanel.visibleZoomLabel": "レイヤー表示のズーム範囲", "xpack.maps.layerPanel.sourceDetailsLabel": "ソースの詳細", - "xpack.maps.layerPanel.sourceSettingsTitle": "ソース設定", "xpack.maps.layerPanel.styleSettingsTitle": "レイヤースタイル", "xpack.maps.layerTocActions.cloneLayerTitle": "レイヤーおクローンを作成", "xpack.maps.layerTocActions.editLayerTitle": "レイヤーを編集", @@ -6690,7 +6683,6 @@ "xpack.maps.layerPanel.settingsPanel.percentageLabel": "%", "xpack.maps.layerPanel.settingsPanel.visibleZoom": "ズームレベル", "xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "ソートフィールドを選択", - "xpack.maps.source.esSearch.sortLabel": "並べ替え", "xpack.maps.toolbarOverlay.drawBoundsLabelShort": "境界を描く", "xpack.maps.toolbarOverlay.drawShapeLabelShort": "図形を描く", "xpack.maps.tooltipSelector.addLabelWithCount": "{count} を追加", @@ -6900,8 +6892,6 @@ "xpack.ml.datavisualizer.selector.startTrialButtonLabel": "トライアルを開始", "xpack.ml.datavisualizer.selector.startTrialTitle": "トライアルを開始", "xpack.ml.datavisualizer.selector.uploadFileButtonLabel": "ファイルをアップロード", - "xpack.ml.datavisualizer.startTrial.fullMLFeaturesDescription": "{platinumSubscriptionLink} が提供するすべての機械学習機能を体験するには、30 日間のトライアルを開始してください。", - "xpack.ml.datavisualizer.startTrial.platinumSubscriptionTitle": "プラチナサブスクリプション", "xpack.ml.dataVisualizerPageLabel": "データビジュアライザー", "xpack.ml.explorer.annotationsTitle": "注釈", "xpack.ml.explorer.anomaliesTitle": "異常", @@ -7120,7 +7110,6 @@ "xpack.ml.itemsGrid.noItemsAddedTitle": "項目が追加されていません", "xpack.ml.itemsGrid.noMatchingItemsTitle": "一致する項目が見つかりません。", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高度な構成", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "ジョブを作成", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "マルチメトリック", "xpack.ml.jobsBreadcrumbs.populationLabel": "集団", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "インデックスまたは検索を選択", @@ -7695,7 +7684,6 @@ "xpack.ml.validateJob.modal.linkToJobTipsText.mlJobTipsLinkText": "機械学習ジョブのヒント", "xpack.ml.validateJob.modal.validateJobTitle": "ジョブ {title} の検証", "xpack.ml.validateJob.validateJobButtonLabel": "ジョブを検証", - "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel": "分析", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "より高度なユースケースでは、ジョブの作成にすべてのオプションを使用します", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高度な設定", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "高度なジョブウィザードでジョブを作成し、このデータの異常を検出します:", @@ -7871,7 +7859,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", - "xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "ジョブ ID {jobId}", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", @@ -8047,7 +8034,6 @@ "xpack.ml.overview.statsBar.runningAnalyticsLabel": "実行中", "xpack.ml.overview.statsBar.stoppedAnalyticsLabel": "停止中", "xpack.ml.overview.statsBar.totalAnalyticsLabel": "分析ジョブ合計", - "xpack.ml.overviewBreadcrumbs.overviewLabel": "概要", "xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel": "アクティブな ML ノード", "xpack.ml.overviewJobsList.statsBar.closedJobsLabel": "ジョブを作成", "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失敗したジョブ", @@ -12736,4 +12722,4 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "ライセンスを更新", "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "ご使用の {licenseType} ライセンスは期限切れです" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 974467a8d20d04..cd18355317954c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -463,9 +463,6 @@ "common.ui.management.breadcrumb": "管理", "common.ui.management.connectDataDisplayName": "连接数据", "common.ui.management.displayName": "管理", - "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", - "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", - "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", "common.ui.management.nav.menu": "管理菜单", "common.ui.modals.cancelButtonLabel": "取消", "common.ui.notify.fatalError.errorStatusMessage": "错误 {errStatus} {errStatusText}:{errMessage}", @@ -567,6 +564,9 @@ "common.ui.aggTypes.scaleMetricsLabel": "缩放指标值(已弃用)", "common.ui.aggTypes.scaleMetricsTooltip": "如果选择手动最小时间间隔并将使用较大的时间间隔,则启用此设置将使计数和求和指标缩放到手动选择的时间间隔。", "common.ui.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 且 {lt} {to}", + "management.editIndexPattern.createIndex.defaultButtonDescription": "对任何数据执行完全聚合", + "management.editIndexPattern.createIndex.defaultButtonText": "标准索引模式", + "management.editIndexPattern.createIndex.defaultTypeName": "索引模式", "core.ui.overlays.banner.attentionTitle": "注意", "core.ui.overlays.banner.closeButtonLabel": "关闭", "core.ui.chrome.headerGlobalNav.goHomePageIconAriaLabel": "前往主页", @@ -5172,10 +5172,8 @@ "xpack.graph.savedWorkspaces.graphWorkspacesLabel": "Graph 工作空间", "xpack.graph.saveWorkspace.successNotification.noDataSavedText": "配置会被保存,但不保存数据", "xpack.graph.saveWorkspace.successNotificationTitle": "已保存“{workspaceTitle}”", - "xpack.graph.serverSideErrors.expiredLicenseErrorMessage": "Graph 不可用 - 许可已过期。", "xpack.graph.serverSideErrors.unavailableGraphErrorMessage": "Graph 不可用", "xpack.graph.serverSideErrors.unavailableLicenseInformationErrorMessage": "Graph 不可用 - 许可信息当前不可用。", - "xpack.graph.serverSideErrors.wrongLicenseTypeErrorMessage": "当前{licenseType}许可的 Graph 不可用。请升级您的许可。", "xpack.graph.settings.advancedSettings.certaintyInputHelpText": "在引入相关字词之前作为证据所需的最小文档数量。", "xpack.graph.settings.advancedSettings.certaintyInputLabel": "确定性", "xpack.graph.settings.advancedSettings.diversityFieldInputHelpText1": "为避免文档示例过于雷同,请选取有助于识别偏差来源的字段。", @@ -5999,10 +5997,8 @@ "xpack.infra.metricsExplorer.openInTSVB": "在 Visualize 中打开", "xpack.infra.metricsExplorer.viewNodeDetail": "查看 {name} 的指标", "xpack.infra.node.ariaLabel": "{nodeName},单击打开菜单", - "xpack.infra.nodeContextMenu.viewAPMTraces": "查看 {nodeType} APM 跟踪", "xpack.infra.nodeContextMenu.viewLogsName": "查看日志", "xpack.infra.nodeContextMenu.viewMetricsName": "查看指标", - "xpack.infra.nodeContextMenu.viewUptimeLink": "在 Uptime 中查看 {nodeType}", "xpack.infra.nodesToWaffleMap.groupsWithGroups.allName": "全部", "xpack.infra.nodesToWaffleMap.groupsWithNodes.allName": "全部", "xpack.infra.notFoundPage.noContentFoundErrorTitle": "未找到任何内容", @@ -6060,7 +6056,6 @@ "xpack.infra.waffle.metricOptions.outboundTrafficText": "出站流量", "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", - "xpack.infra.waffle.nodeTypeSwitcher.hostsLabel": "主机", "xpack.infra.waffle.selectTwoGroupingsTitle": "选择最多两个分组", "xpack.infra.waffle.unableToSelectGroupErrorMessage": "无法选择 {nodeType} 的分组依据选项", "xpack.infra.waffle.unableToSelectMetricErrorTitle": "无法选择指标选项或指标值。", @@ -6435,7 +6430,6 @@ "xpack.maps.layerControl.tocEntry.hideDetailsButtonTitle": "隐藏图层详情", "xpack.maps.layerControl.tocEntry.showDetailsButtonAriaLabel": "显示图层详情", "xpack.maps.layerControl.tocEntry.showDetailsButtonTitle": "显示图层详情", - "xpack.maps.layerPanel.applyGlobalQueryCheckboxLabel": "将全局筛选应用到图层", "xpack.maps.layerPanel.filterEditor.addFilterButtonLabel": "添加筛选", "xpack.maps.layerPanel.filterEditor.editFilterButtonLabel": "编辑筛选", "xpack.maps.layerPanel.filterEditor.emptyState.description": "添加筛选以缩小图层数据范围。", @@ -6470,7 +6464,6 @@ "xpack.maps.layerPanel.settingsPanel.unableToLoadTitle": "无法加载图层", "xpack.maps.layerPanel.settingsPanel.visibleZoomLabel": "图层可见性的缩放范围", "xpack.maps.layerPanel.sourceDetailsLabel": "源详情", - "xpack.maps.layerPanel.sourceSettingsTitle": "源设置", "xpack.maps.layerPanel.styleSettingsTitle": "图层样式", "xpack.maps.layerTocActions.cloneLayerTitle": "克隆图层", "xpack.maps.layerTocActions.editLayerTitle": "编辑图层", @@ -6692,7 +6685,6 @@ "xpack.maps.layerPanel.settingsPanel.percentageLabel": "%", "xpack.maps.layerPanel.settingsPanel.visibleZoom": "缩放级别", "xpack.maps.source.esSearch.sortFieldSelectPlaceholder": "选择排序字段", - "xpack.maps.source.esSearch.sortLabel": "排序", "xpack.maps.toolbarOverlay.drawBoundsLabelShort": "绘制边界", "xpack.maps.toolbarOverlay.drawShapeLabelShort": "绘制形状", "xpack.maps.tooltipSelector.addLabelWithCount": "添加 {count} 个", @@ -6902,8 +6894,6 @@ "xpack.ml.datavisualizer.selector.startTrialButtonLabel": "开始试用", "xpack.ml.datavisualizer.selector.startTrialTitle": "开始试用", "xpack.ml.datavisualizer.selector.uploadFileButtonLabel": "上传文件", - "xpack.ml.datavisualizer.startTrial.fullMLFeaturesDescription": "要体验 {platinumSubscriptionLink} 提供的完整 Machine Learning 功能,可以安装为期 30 天的试用版。", - "xpack.ml.datavisualizer.startTrial.platinumSubscriptionTitle": "白金级订阅", "xpack.ml.dataVisualizerPageLabel": "数据可视化工具", "xpack.ml.explorer.annotationsTitle": "注释", "xpack.ml.explorer.anomaliesTitle": "异常", @@ -7122,7 +7112,6 @@ "xpack.ml.itemsGrid.noItemsAddedTitle": "没有添加任何项", "xpack.ml.itemsGrid.noMatchingItemsTitle": "没有匹配的项", "xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel": "高级配置", - "xpack.ml.jobsBreadcrumbs.createJobLabel": "创建作业", "xpack.ml.jobsBreadcrumbs.multiMetricLabel": "多指标", "xpack.ml.jobsBreadcrumbs.populationLabel": "填充", "xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel": "选择索引或搜索", @@ -7788,7 +7777,6 @@ "xpack.ml.dataframe.analyticsList.type": "类型", "xpack.ml.dataframe.analyticsList.viewActionName": "查看", "xpack.ml.dataframe.analyticsList.viewAriaLabel": "查看", - "xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameLabel": "分析", "xpack.ml.datavisualizer.actionsPanel.advancedDescription": "使用全部选项为更高级的用例创建作业", "xpack.ml.datavisualizer.actionsPanel.advancedTitle": "高级", "xpack.ml.datavisualizer.actionsPanel.createJobDescription": "使用“高级作业”向导创建作业,以查找此数据中的异常:", @@ -7964,7 +7952,6 @@ "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", - "xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "作业 ID {jobId}", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", @@ -8140,7 +8127,6 @@ "xpack.ml.overview.statsBar.runningAnalyticsLabel": "正在运行", "xpack.ml.overview.statsBar.stoppedAnalyticsLabel": "已停止", "xpack.ml.overview.statsBar.totalAnalyticsLabel": "分析作业总数", - "xpack.ml.overviewBreadcrumbs.overviewLabel": "概览", "xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel": "活动 ML 节点", "xpack.ml.overviewJobsList.statsBar.closedJobsLabel": "已关闭的作业", "xpack.ml.overviewJobsList.statsBar.failedJobsLabel": "失败的作业", @@ -12825,4 +12811,4 @@ "xpack.licensing.welcomeBanner.licenseIsExpiredDescription.updateYourLicenseLinkText": "更新您的许可", "xpack.licensing.welcomeBanner.licenseIsExpiredTitle": "您的{licenseType}许可已过期" } -} +} \ No newline at end of file diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index bce6a677453777..5b18b6be9e3a4e 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -17,7 +17,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); afterEach(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('meets a11y requirements', async () => { diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index e1a90efccaa807..a9ac7c71d3e79e 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -13,9 +13,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { return { ...functionalConfig.getAll(), - testFiles: [require.resolve('./apps/login_page')], - pageObjects, services, diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts index 6ef30a6f933ff5..73279cd0c2ff0c 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/index.ts @@ -32,6 +32,30 @@ export default function(kibana: any) { require: ['actions'], name: NAME, init: (server: Hapi.Server) => { + server.plugins.xpack_main.registerFeature({ + id: 'actions', + name: 'Actions', + app: ['actions', 'kibana'], + privileges: { + all: { + savedObject: { + all: ['action', 'action_task_params'], + read: [], + }, + ui: [], + api: ['actions-read', 'actions-all'], + }, + read: { + savedObject: { + all: ['action_task_params'], + read: ['action'], + }, + ui: [], + api: ['actions-read'], + }, + }, + }); + initSlack(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.SLACK)); initWebhook(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.WEBHOOK)); initPagerduty(server, getExternalServiceSimulatorPath(ExternalServiceSimulator.PAGERDUTY)); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts index 30b235a784c224..ebe741df71d79f 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/index.ts @@ -14,6 +14,30 @@ export default function(kibana: any) { require: ['actions', 'alerting', 'elasticsearch'], name: 'alerts', init(server: any) { + server.plugins.xpack_main.registerFeature({ + id: 'alerting', + name: 'Alerting', + app: ['alerting', 'kibana'], + privileges: { + all: { + savedObject: { + all: ['alert'], + read: [], + }, + ui: [], + api: ['alerting-read', 'alerting-all'], + }, + read: { + savedObject: { + all: [], + read: ['alert'], + }, + ui: [], + api: ['alerting-read'], + }, + }, + }); + // Action types const noopActionType: ActionType = { id: 'test.noop', diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index a24155943b45c4..f613473dd87fb0 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -114,8 +114,6 @@ export default function({ getService }: FtrProviderContext) { 'maps', 'uptime', 'siem', - 'alerting', - 'actions', ].sort() ); }); diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index ed0e6488320d44..fd700b41df563d 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -28,5 +28,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); loadTestFile(require.resolve('./endpoint')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/infra/waffle.ts b/x-pack/test/api_integration/apis/infra/waffle.ts index 41bdb089329999..1f79ad4eee4e52 100644 --- a/x-pack/test/api_integration/apis/infra/waffle.ts +++ b/x-pack/test/api_integration/apis/infra/waffle.ts @@ -189,9 +189,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); } }); @@ -279,9 +279,9 @@ export default function({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metric'); expect(firstNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); const secondNode = nodes[1]; expect(secondNode).to.have.property('path'); @@ -291,9 +291,9 @@ export default function({ getService }: FtrProviderContext) { expect(secondNode).to.have.property('metric'); expect(secondNode.metric).to.eql({ name: 'cpu', - value: 0.003666666666666667, - avg: 0.00809090909090909, - max: 0.057833333333333334, + value: 0.009285714285714286, + max: 0.009285714285714286, + avg: 0.0015476190476190477, }); } }); diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts new file mode 100644 index 00000000000000..b5e5168621584d --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -0,0 +1,90 @@ +/* + * 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'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const testDataList = [ + { + testTitleSuffix: 'with 1 field, 1 agg, no split', + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '15m', ms: 900000 }, + }, + }, + { + testTitleSuffix: 'with 2 fields, 2 aggs, no split', + requestBody: { + aggTypes: ['avg', 'sum'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['products.base_price', 'products.base_unit_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '30m', ms: 1800000 }, + }, + }, + { + testTitleSuffix: 'with 1 field, 1 agg, 1 split with cardinality 46', + requestBody: { + aggTypes: ['avg'], + duration: { start: 1560297859000, end: 1562975136000 }, + fields: ['taxless_total_price'], + index: 'ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + splitField: 'customer_first_name.keyword', + timeField: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { name: '3h', ms: 10800000 }, + }, + }, +]; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('bucket span estimator', () => { + before(async () => { + await esArchiver.load('ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + }); + + for (const testData of testDataList) { + it(`estimates the bucket span ${testData.testTitleSuffix}`, async () => { + const { body } = await supertest + .post('/api/ml/validate/estimate_bucket_span') + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + expect(body).to.eql(testData.expected.responseBody); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts new file mode 100644 index 00000000000000..2e0521e2b82737 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -0,0 +1,15 @@ +/* + * 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 default function({ loadTestFile }: FtrProviderContext) { + describe('Machine Learning', function() { + this.tags(['mlqa']); + + loadTestFile(require.resolve('./bucket_span_estimator')); + }); +} diff --git a/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js b/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js index bea09562bdb11e..fa26a0dcac794e 100644 --- a/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js +++ b/x-pack/test/api_integration/apis/monitoring/logstash/multicluster_pipelines.js @@ -11,7 +11,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('pipelines listing multicluster', () => { + describe.skip('pipelines listing multicluster', () => { const archive = 'monitoring/logstash_pipelines_multicluster'; const timeRange = { min: '2019-11-11T15:13:45.266Z', diff --git a/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js b/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js index 0852b8293886eb..9e2160a69c726f 100644 --- a/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js +++ b/x-pack/test/api_integration/apis/monitoring/logstash/pipelines.js @@ -11,7 +11,7 @@ export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - describe('pipelines', () => { + describe.skip('pipelines', () => { const archive = 'monitoring/logstash/changing_pipelines'; const timeRange = { min: '2019-11-04T15:40:44.855Z', diff --git a/x-pack/test/api_integration/apis/monitoring/setup/index.js b/x-pack/test/api_integration/apis/monitoring/setup/index.js index a6bb46740e940f..89e970c8002fd2 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/index.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/index.js @@ -5,7 +5,10 @@ */ export default function ({ loadTestFile }) { - describe('Setup', () => { + describe('Setup', function () { + // Setup mode is not supported in cloud + this.tags(['skipCloud']); + loadTestFile(require.resolve('./collection')); }); } diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index 1d10b3f8803a56..cd85e6906d65ec 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -32,7 +32,7 @@ export default function ({ getService }) { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -41,24 +41,24 @@ export default function ({ getService }) { const wrongUsername = `wrong-${validUsername}`; const wrongPassword = `wrong-${validPassword}`; - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: wrongUsername, password: wrongPassword }) .expect(401); - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: wrongPassword }) .expect(401); - await supertest.post('/api/security/v1/login') + await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: wrongUsername, password: validPassword }) .expect(401); }); it('should set authentication cookie for login with valid credentials', async () => { - const loginResponse = await supertest.post('/api/security/v1/login') + const loginResponse = await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204); @@ -77,17 +77,17 @@ export default function ({ getService }) { const wrongUsername = `wrong-${validUsername}`; const wrongPassword = `wrong-${validPassword}`; - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${wrongUsername}:${wrongPassword}`).toString('base64')}`) .expect(401); - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${validUsername}:${wrongPassword}`).toString('base64')}`) .expect(401); - await supertest.get('/api/security/v1/me') + await supertest.get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${wrongUsername}:${validPassword}`).toString('base64')}`) .expect(401); @@ -95,7 +95,7 @@ export default function ({ getService }) { it('should allow access to the API with valid credentials in the header', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', `Basic ${Buffer.from(`${validUsername}:${validPassword}`).toString('base64')}`) .expect(200); @@ -116,7 +116,7 @@ export default function ({ getService }) { describe('with session cookie', () => { let sessionCookie; beforeEach(async () => { - const loginResponse = await supertest.post('/api/security/v1/login') + const loginResponse = await supertest.post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204); @@ -128,12 +128,12 @@ export default function ({ getService }) { // There is no session cookie provided and no server side session should have // been established, so request should be rejected. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -153,7 +153,7 @@ export default function ({ getService }) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -165,7 +165,7 @@ export default function ({ getService }) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -179,7 +179,7 @@ export default function ({ getService }) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -190,7 +190,7 @@ export default function ({ getService }) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Bearer AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -200,7 +200,7 @@ export default function ({ getService }) { }); it('should clear cookie on logout and redirect to login', async ()=> { - const logoutResponse = await supertest.get('/api/security/v1/logout?next=%2Fabc%2Fxyz&msg=test') + const logoutResponse = await supertest.get('/api/security/logout?next=%2Fabc%2Fxyz&msg=test') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -256,7 +256,7 @@ export default function ({ getService }) { }); it('should redirect to home page if cookie is not provided', async ()=> { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); diff --git a/x-pack/test/api_integration/apis/security/change_password.ts b/x-pack/test/api_integration/apis/security/change_password.ts index 09751d4a3641af..3efb7eb2bb1ddf 100644 --- a/x-pack/test/api_integration/apis/security/change_password.ts +++ b/x-pack/test/api_integration/apis/security/change_password.ts @@ -20,7 +20,7 @@ export default function({ getService }: FtrProviderContext) { await security.user.create(mockUserName, { password: mockUserPassword, roles: [] }); const loginResponse = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(204); @@ -34,7 +34,7 @@ export default function({ getService }: FtrProviderContext) { const newPassword = `xxx-${mockUserPassword}-xxx`; await supertest - .post(`/api/security/v1/users/${mockUserName}/password`) + .post(`/internal/security/users/${mockUserName}/password`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .send({ password: wrongPassword, newPassword }) @@ -42,21 +42,21 @@ export default function({ getService }: FtrProviderContext) { // Let's check that we can't login with wrong password, just in case. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: wrongPassword }) .expect(401); // Let's check that we can't login with the password we were supposed to set. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: newPassword }) .expect(401); // And can login with the current password. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(204); @@ -66,7 +66,7 @@ export default function({ getService }: FtrProviderContext) { const newPassword = `xxx-${mockUserPassword}-xxx`; const passwordChangeResponse = await supertest - .post(`/api/security/v1/users/${mockUserName}/password`) + .post(`/internal/security/users/${mockUserName}/password`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .send({ password: mockUserPassword, newPassword }) @@ -76,28 +76,28 @@ export default function({ getService }: FtrProviderContext) { // Let's check that previous cookie isn't valid anymore. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); // And that we can't login with the old password. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: mockUserPassword }) .expect(401); // But new cookie should be valid. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); // And that we can login with new credentials. await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: mockUserName, password: newPassword }) .expect(204); diff --git a/x-pack/test/api_integration/apis/security/index_fields.ts b/x-pack/test/api_integration/apis/security/index_fields.ts index 60c6e800c40b23..7adc589fbec3ed 100644 --- a/x-pack/test/api_integration/apis/security/index_fields.ts +++ b/x-pack/test/api_integration/apis/security/index_fields.ts @@ -11,10 +11,10 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('Index Fields', () => { - describe('GET /api/security/v1/fields/{query}', () => { + describe('GET /internal/security/fields/{query}', () => { it('should return a list of available index mapping fields', async () => { await supertest - .get('/api/security/v1/fields/.kibana') + .get('/internal/security/fields/.kibana') .set('kbn-xsrf', 'xxx') .send() .expect(200) diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index d6ad1608f36883..d4c8a3e68c50ed 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -37,8 +37,6 @@ export default function({ getService }: FtrProviderContext) { uptime: ['all', 'read'], apm: ['all', 'read'], siem: ['all', 'read'], - actions: ['all', 'read'], - alerting: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], diff --git a/x-pack/test/api_integration/apis/security/session.ts b/x-pack/test/api_integration/apis/security/session.ts index 7c7883f58cb30e..5d0935bb1ae2d7 100644 --- a/x-pack/test/api_integration/apis/security/session.ts +++ b/x-pack/test/api_integration/apis/security/session.ts @@ -43,7 +43,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username: validUsername, password: validPassword }) .expect(204) diff --git a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts index 06fd971399ea3e..db5e11ef367ad7 100644 --- a/x-pack/test/api_integration/apis/short_urls/feature_controls.ts +++ b/x-pack/test/api_integration/apis/short_urls/feature_controls.ts @@ -107,7 +107,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) expect(resp.status).to.eql(302); expect(resp.headers.location).to.eql('/app/kibana#foo/bar/baz'); } else { - expect(resp.status).to.eql(500); + expect(resp.status).to.eql(403); expect(resp.headers.location).to.eql(undefined); } }); diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 15185504075290..12a1576f789824 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../plugins/security/server/elasticsearch_client_plugin'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -19,6 +19,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin], + plugins: [elasticsearchClientPlugin], }); } diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index d60b286e3337ae..f148d62421ff8b 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -46,7 +46,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_advanced_settings_all_user', 'global_advanced_settings_all_user-password', @@ -62,7 +62,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_advanced_settings_all_role'), security.user.delete('global_advanced_settings_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index e9e14362413086..a58eb61ec4ca2b 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -45,7 +45,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_canvas_all_user', @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_canvas_all_role'), security.user.delete('global_canvas_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 9e5447919c6d02..8acb875e1d7b47 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -110,11 +110,11 @@ export default function ({ getService, getPageObjects }) { }); after('logout', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('shows only the dashboard app link', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('dashuser', '123456'); const appLinks = await appsMenu.readLinks(); @@ -194,7 +194,7 @@ export default function ({ getService, getPageObjects }) { }); it('is loaded for a user who is assigned a non-dashboard mode role', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('mixeduser', '123456'); if (await appsMenu.linkExists('Management')) { @@ -203,7 +203,7 @@ export default function ({ getService, getPageObjects }) { }); it('is not loaded for a user who is assigned a superuser role', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('mysuperuser', '123456'); if (!await appsMenu.linkExists('Management')) { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index f5ceae689ead22..553ce459ebb182 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -33,14 +33,14 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.loadIfNeeded('logstash_functional'); // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { await esArchiver.unload('discover/feature_controls/security'); // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); describe('global discover all privileges', () => { diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index cce5ededf2cf36..4929bb52c170c4 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -46,7 +46,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_index_patterns_all_user', @@ -64,7 +64,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_index_patterns_all_role'), security.user.delete('global_index_patterns_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts index 08cb5b3951ee4a..8b2df502dc100d 100644 --- a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -29,7 +29,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { @@ -37,7 +37,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await security.role.delete('global_all_role'); // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); describe('machine_learning_user', () => { diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index 2543eec76340f4..cf31f445a96f3f 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -49,7 +49,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('global_maps_all_user', 'global_maps_all_user-password', { expectSpaceSelector: false, @@ -60,7 +60,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_maps_all_role'), security.user.delete('global_maps_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/maps/layer_errors.js b/x-pack/test/functional/apps/maps/layer_errors.js index 142ea7c4bf0252..36a6e48eb88ef4 100644 --- a/x-pack/test/functional/apps/maps/layer_errors.js +++ b/x-pack/test/functional/apps/maps/layer_errors.js @@ -65,7 +65,8 @@ export default function ({ getPageObjects }) { }); }); - describe('EMSFileSource with missing EMS id', () => { + // FLAKY: https://github.com/elastic/kibana/issues/36011 + describe.skip('EMSFileSource with missing EMS id', () => { const MISSING_EMS_ID = 'idThatDoesNotExitForEMSFileSource'; const LAYER_NAME = 'EMS_vector_shapes'; diff --git a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js index c1faa25ed9c70a..58756c657347d6 100644 --- a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js +++ b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js @@ -35,7 +35,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { }, async tearDown() { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.user.delete('basic_monitoring_user'); return esArchiver.unload(_archive); } diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index aa96d712287990..2f6b1a91b7304d 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -13,7 +13,7 @@ export default function ({ getService, getPageObjects }) { const clusterOverview = getService('monitoringClusterOverview'); const retry = getService('retry'); - describe('Monitoring is turned off', () => { + describe('Monitoring is turned off', function () { before(async () => { const browser = getService('browser'); await browser.setWindowSize(1600, 1000); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts index 60aa40d0b59047..80f33ff6175c56 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -21,7 +21,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esArchiver.unload('empty_kibana'); await PageObjects.common.navigateToApp('home'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); describe('space with no features disabled', () => { diff --git a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js index f4d2a5a4a20a5e..3aacd9e66dd4ad 100644 --- a/x-pack/test/functional/apps/monitoring/logstash/pipelines.js +++ b/x-pack/test/functional/apps/monitoring/logstash/pipelines.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { const pipelinesList = getService('monitoringLogstashPipelines'); const lsClusterSummaryStatus = getService('monitoringLogstashSummaryStatus'); - describe('Logstash pipelines', () => { + describe.skip('Logstash pipelines', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); before(async () => { diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 619a85c616b2e2..53da2b80454e4a 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -44,7 +44,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('global_all_user', 'global_all_user-password', { expectSpaceSelector: false, @@ -55,7 +55,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_all_role'), security.user.delete('global_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); @@ -162,7 +162,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('global_som_read_user', 'global_som_read_user-password', { expectSpaceSelector: false, @@ -173,7 +173,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_som_read_role'), security.user.delete('global_som_read_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); @@ -281,7 +281,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_visualize_all_user', @@ -296,7 +296,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_visualize_all_role'), security.user.delete('global_visualize_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 9dac150b9a0e6b..8a4184daf4d23f 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -66,7 +66,7 @@ export default function ({ getService, getPageObjects }) { }); it('user East should only see EAST doc', async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('userEast', 'changeme'); await PageObjects.common.navigateToApp('discover'); await retry.try(async () => { @@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }) { expect(rowData).to.be('name:ABC Company region:EAST _id:doc1 _type: - _index:dlstest _score:0'); }); after('logout', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); }); } diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 2939805d708598..8bcc49e7dd5545 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -89,7 +89,7 @@ export default function ({ getService, getPageObjects }) { }); it('user customer1 should see ssn', async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('customer1', 'changeme'); await PageObjects.common.navigateToApp('discover'); await retry.tryForTime(10000, async () => { @@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }) { }); it('user customer2 should not see ssn', async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('customer2', 'changeme'); await PageObjects.common.navigateToApp('discover'); await retry.tryForTime(10000, async () => { @@ -114,7 +114,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); }); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 3f9f2f6bdbe87d..cf81af54c6d02e 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -79,7 +79,7 @@ export default function ({ getService, getPageObjects }) { expect(user.roles).to.eql(['rbac_read']); expect(user.fullname).to.eql('kibanareadonlyFirst kibanareadonlyLast'); expect(user.reserved).to.be(false); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); @@ -100,11 +100,11 @@ export default function ({ getService, getPageObjects }) { await PageObjects.timePicker.setDefaultAbsoluteRange(); await PageObjects.visualize.waitForVisualization(); await PageObjects.visualize.saveVisualizationExpectSuccess(vizName1); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); }); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 4f725d04ce034c..0c84a34e0bf89a 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }) { expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_user']); expect(users.Rashmi.fullname).to.eql('RashmiFirst RashmiLast'); expect(users.Rashmi.reserved).to.be(false); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('Rashmi', 'changeme'); }); @@ -83,7 +83,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); }); diff --git a/x-pack/test/functional/apps/security/security.js b/x-pack/test/functional/apps/security/security.js index 537491185d9ef5..ca7aa893d14e1f 100644 --- a/x-pack/test/functional/apps/security/security.js +++ b/x-pack/test/functional/apps/security/security.js @@ -16,7 +16,7 @@ export default function ({ getService, getPageObjects }) { describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) { }); afterEach(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('can login', async () => { diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index 538066d19ff1f1..c04a8031a03790 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }) { expect(users.newuser.fullname).to.eql('newuserFirst newuserLast'); expect(users.newuser.email).to.eql('newuser@myEmail.com'); expect(users.newuser.reserved).to.be(false); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('login as new user and verify email', async function () { @@ -41,7 +41,7 @@ export default function ({ getService, getPageObjects }) { it('click changepassword link, change the password and re-login', async function () { await PageObjects.accountSetting.verifyAccountSettings('newuser@myEmail.com', 'newuser'); await PageObjects.accountSetting.changePassword('changeme', 'mechange'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); @@ -51,7 +51,7 @@ export default function ({ getService, getPageObjects }) { }); after(async function () { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); }); } diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 017d252b166cc4..b931a5cb0ca63d 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -19,7 +19,7 @@ export default function enterSpaceFunctonalTests({ after(async () => await esArchiver.unload('spaces/enter_space')); afterEach(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('allows user to navigate to different spaces, respecting the configured default route', async () => { diff --git a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts index 1e74322d0676d6..46f0be1e6f6d65 100644 --- a/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts +++ b/x-pack/test/functional/apps/spaces/feature_controls/spaces_security.ts @@ -39,7 +39,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('global_all_user', 'global_all_user-password', { expectSpaceSelector: false, @@ -50,7 +50,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('global_all_role'), security.user.delete('global_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); @@ -111,7 +111,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'default_space_all_user', @@ -126,7 +126,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await Promise.all([ security.role.delete('default_space_all_role'), security.user.delete('default_space_all_user'), - PageObjects.security.logout(), + PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 9b4c99334dd672..3d1ef40262b1dd 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -26,7 +26,7 @@ export default function spaceSelectorFunctonalTests({ after(async () => await esArchiver.unload('spaces/selector')); afterEach(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('allows user to navigate to different spaces', async () => { @@ -87,7 +87,7 @@ export default function spaceSelectorFunctonalTests({ hash: sampleDataHash, }); await PageObjects.home.removeSampleDataSet('logs'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await esArchiver.unload('spaces/selector'); }); diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index ffd87ff74588cf..58551aaaf41127 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -18,7 +18,7 @@ export default function statusPageFunctonalTests({ after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.statusPage.navigateToPage(); await PageObjects.statusPage.expectStatusPage(); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index 073d54b9088bbf..64fb218a62c80f 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -41,7 +41,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_timelion_all_user', @@ -53,7 +53,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('global_timelion_all_role'); await security.user.delete('global_timelion_all_user'); }); @@ -107,7 +107,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('global_timelion_read_role'); await security.user.delete('global_timelion_read_user'); }); @@ -151,7 +151,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'no_timelion_privileges_user', @@ -163,7 +163,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('no_timelion_privileges_role'); await security.user.delete('no_timelion_privileges_user'); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index d85271fe166b1d..86fe606ecafad5 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -56,7 +56,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { full_name: 'test user', }); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login( 'global_visualize_all_user', @@ -68,7 +68,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('global_visualize_all_role'); await security.user.delete('global_visualize_all_user'); }); @@ -184,7 +184,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('global_visualize_read_role'); await security.user.delete('global_visualize_read_user'); }); @@ -294,7 +294,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await security.role.delete('no_visualize_privileges_role'); await security.user.delete('no_visualize_privileges_user'); }); diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 1995f377829480..1f3711ff5e506c 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +export type MlApi = ProvidedType; + export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const log = getService('log'); diff --git a/x-pack/test/functional/services/machine_learning/common.ts b/x-pack/test/functional/services/machine_learning/common.ts index 12b9e8a1cfb294..35ee32fa5d94ee 100644 --- a/x-pack/test/functional/services/machine_learning/common.ts +++ b/x-pack/test/functional/services/machine_learning/common.ts @@ -3,6 +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 { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -11,6 +12,8 @@ interface SetValueOptions { typeCharByChar?: boolean; } +export type MlCommon = ProvidedType; + export function MachineLearningCommonProvider({ getService }: FtrProviderContext) { const log = getService('log'); const retry = getService('retry'); diff --git a/x-pack/test/functional/services/machine_learning/custom_urls.ts b/x-pack/test/functional/services/machine_learning/custom_urls.ts index dc6e4a2fccb106..68429084620184 100644 --- a/x-pack/test/functional/services/machine_learning/custom_urls.ts +++ b/x-pack/test/functional/services/machine_learning/custom_urls.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +export type MlCustomUrls = ProvidedType; + export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index 163c3c60ffdabc..8c8b5db1d2c521 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningAPIProvider } from './api'; +import { MlApi } from './api'; import { DATA_FRAME_TASK_STATE } from '../../../../legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common'; export function MachineLearningDataFrameAnalyticsProvider( { getService }: FtrProviderContext, - mlApi: ProvidedType + mlApi: MlApi ) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index 5ffb235a828d6b..1fa1f62a9ae119 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -3,16 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningAPIProvider } from './api'; +import { MlApi } from './api'; import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; export function MachineLearningJobManagementProvider( { getService }: FtrProviderContext, - mlApi: ProvidedType + mlApi: MlApi ) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts index ab53b0412ca353..755091ca10f3b6 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningCommonProvider } from './common'; +import { MlCommon } from './common'; export function MachineLearningJobWizardAdvancedProvider( { getService }: FtrProviderContext, - mlCommon: ProvidedType + mlCommon: MlCommon ) { const comboBox = getService('comboBox'); const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index b9e6822c8f41a9..c2f408276d9e45 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -4,16 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MachineLearningCommonProvider } from './common'; -import { MachineLearningCustomUrlsProvider } from './custom_urls'; +import { MlCommon } from './common'; +import { MlCustomUrls } from './custom_urls'; export function MachineLearningJobWizardCommonProvider( { getService }: FtrProviderContext, - mlCommon: ProvidedType, - customUrls: ProvidedType + mlCommon: MlCommon, + customUrls: MlCustomUrls ) { const comboBox = getService('comboBox'); const retry = getService('retry'); diff --git a/x-pack/test/functional/services/monitoring/logstash_pipelines.js b/x-pack/test/functional/services/monitoring/logstash_pipelines.js index 759728555c86ad..6740d6352bbf7d 100644 --- a/x-pack/test/functional/services/monitoring/logstash_pipelines.js +++ b/x-pack/test/functional/services/monitoring/logstash_pipelines.js @@ -10,6 +10,7 @@ export function MonitoringLogstashPipelinesProvider({ getService, getPageObjects const testSubjects = getService('testSubjects'); const retry = getService('retry'); const PageObjects = getPageObjects(['monitoring']); + const find = getService('find'); const SUBJ_LISTING_PAGE = 'logstashPipelinesListing'; @@ -34,6 +35,12 @@ export function MonitoringLogstashPipelinesProvider({ getService, getPageObjects return PageObjects.monitoring.tableGetRowsFromContainer(SUBJ_TABLE_CONTAINER); } + async waitForTableToFinishLoading() { + await retry.try(async () => { + await find.waitForDeletedByCssSelector('.euiBasicTable-loading', 5000); + }); + } + async getPipelinesAll() { const ids = await testSubjects.getVisibleTextAll(SUBJ_PIPELINES_IDS); const eventsEmittedRates = await testSubjects.getVisibleTextAll(SUBJ_PIPELINES_EVENTS_EMITTED_RATES); @@ -57,21 +64,25 @@ export function MonitoringLogstashPipelinesProvider({ getService, getPageObjects async clickIdCol() { const headerCell = await testSubjects.find(SUBJ_TABLE_SORT_ID_COL); const button = await headerCell.findByTagName('button'); - return button.click(); + await button.click(); + await this.waitForTableToFinishLoading(); } async clickEventsEmittedRateCol() { const headerCell = await testSubjects.find(SUBJ_TABLE_SORT_EVENTS_EMITTED_RATE_COL); const button = await headerCell.findByTagName('button'); - return button.click(); + await button.click(); + await this.waitForTableToFinishLoading(); } - setFilter(text) { - return PageObjects.monitoring.tableSetFilter(SUBJ_SEARCH_BAR, text); + async setFilter(text) { + await PageObjects.monitoring.tableSetFilter(SUBJ_SEARCH_BAR, text); + await this.waitForTableToFinishLoading(); } - clearFilter() { - return PageObjects.monitoring.tableClearFilter(SUBJ_SEARCH_BAR); + async clearFilter() { + await PageObjects.monitoring.tableClearFilter(SUBJ_SEARCH_BAR); + await this.waitForTableToFinishLoading(); } assertNoData() { diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 450f7b1a427dc4..bd35f21d8f428b 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -47,7 +47,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -55,7 +55,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username, password }) .expect(204); @@ -67,7 +67,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(cookie); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', cookie.cookieString()) .expect(200); @@ -98,7 +98,7 @@ export default function({ getService }: FtrProviderContext) { describe('finishing SPNEGO', () => { it('should properly set cookie and authenticate user', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -114,7 +114,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200, { @@ -134,7 +134,7 @@ export default function({ getService }: FtrProviderContext) { it('should re-initiate SPNEGO handshake if token is rejected with 401', async () => { const spnegoResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${Buffer.from('Hello').toString('base64')}`) .expect(401); expect(spnegoResponse.headers['set-cookie']).to.be(undefined); @@ -143,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail if SPNEGO token is rejected because of unknown reason', async () => { const spnegoResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', 'Negotiate (:I am malformed:)') .expect(500); expect(spnegoResponse.headers['set-cookie']).to.be(undefined); @@ -156,7 +156,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -169,7 +169,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -181,7 +181,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -195,7 +195,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -206,7 +206,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic a3JiNTprcmI1') .set('Cookie', sessionCookie.cookieString()) @@ -220,7 +220,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to `logged_out` page after successful logout', async () => { // First authenticate user to retrieve session cookie. const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -232,7 +232,7 @@ export default function({ getService }: FtrProviderContext) { // And then log user out. const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -245,7 +245,7 @@ export default function({ getService }: FtrProviderContext) { // Token that was stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); @@ -259,7 +259,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout').expect(302); + const logoutResponse = await supertest.get('/api/security/logout').expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); expect(logoutResponse.headers.location).to.be('/'); @@ -271,7 +271,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -292,7 +292,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -305,7 +305,7 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', refreshedCookie.cookieString()) .expect(200); @@ -335,7 +335,7 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', refreshedCookie.cookieString()) .expect(200); @@ -344,12 +344,13 @@ export default function({ getService }: FtrProviderContext) { }); }); - describe('API access with missing access token document or expired refresh token.', () => { + // FAILING: https://github.com/elastic/kibana/issues/52969 + describe.skip('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('Authorization', `Negotiate ${spnegoToken}`) .expect(200); @@ -374,7 +375,7 @@ export default function({ getService }: FtrProviderContext) { it('AJAX call should initiate SPNEGO and clear existing cookie', async function() { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(401); diff --git a/x-pack/test/licensing_plugin/apis/changes.ts b/x-pack/test/licensing_plugin/apis/changes.ts index cbff783a0633c3..cf4fecfa32d949 100644 --- a/x-pack/test/licensing_plugin/apis/changes.ts +++ b/x-pack/test/licensing_plugin/apis/changes.ts @@ -37,7 +37,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); await PageObjects.security.login('license_manager_user', 'license_manager_user-password'); }, @@ -73,7 +73,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }, async getLicense(): Promise { - // > --xpack.licensing.pollingFrequency set in test config + // > --xpack.licensing.api_polling_frequency set in test config // to wait for Kibana server to re-fetch the license from Elasticsearch await delay(1000); @@ -97,30 +97,71 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { isEnabled: true, }); + const { + body: legacyInitialLicense, + headers: legacyInitialLicenseHeaders, + } = await supertest.get('/api/xpack/v1/info').expect(200); + + expect(legacyInitialLicense.license?.type).to.be('basic'); + expect(legacyInitialLicense.features).to.have.property('security'); + expect(legacyInitialLicenseHeaders['kbn-xpack-sig']).to.be.a('string'); + + // license hasn't changed const refetchedLicense = await scenario.getLicense(); expect(refetchedLicense.license?.type).to.be('basic'); expect(refetchedLicense.signature).to.be(initialLicense.signature); + const { + body: legacyRefetchedLicense, + headers: legacyRefetchedLicenseHeaders, + } = await supertest.get('/api/xpack/v1/info').expect(200); + + expect(legacyRefetchedLicense.license?.type).to.be('basic'); + expect(legacyRefetchedLicenseHeaders['kbn-xpack-sig']).to.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + // server allows to request trial only once. // other attempts will throw 403 await scenario.startTrial(); const trialLicense = await scenario.getLicense(); expect(trialLicense.license?.type).to.be('trial'); expect(trialLicense.signature).to.not.be(initialLicense.signature); + expect(trialLicense.features?.security).to.eql({ isAvailable: true, isEnabled: true, }); + const { body: legacyTrialLicense, headers: legacyTrialLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + + expect(legacyTrialLicense.license?.type).to.be('trial'); + expect(legacyTrialLicense.features).to.have.property('security'); + expect(legacyTrialLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + await scenario.startBasic(); const basicLicense = await scenario.getLicense(); expect(basicLicense.license?.type).to.be('basic'); expect(basicLicense.signature).not.to.be(initialLicense.signature); - expect(trialLicense.features?.security).to.eql({ + + expect(basicLicense.features?.security).to.eql({ isAvailable: true, isEnabled: true, }); + const { body: legacyBasicLicense, headers: legacyBasicLicenseHeaders } = await supertest + .get('/api/xpack/v1/info') + .expect(200); + expect(legacyBasicLicense.license?.type).to.be('basic'); + expect(legacyBasicLicense.features).to.have.property('security'); + expect(legacyBasicLicenseHeaders['kbn-xpack-sig']).to.not.be( + legacyInitialLicenseHeaders['kbn-xpack-sig'] + ); + await scenario.deleteLicense(); const inactiveLicense = await scenario.getLicense(); expect(inactiveLicense.signature).to.not.be(initialLicense.signature); diff --git a/x-pack/test/licensing_plugin/config.ts b/x-pack/test/licensing_plugin/config.ts index 810dd3edc76b9d..9a83a6f6b5a0b9 100644 --- a/x-pack/test/licensing_plugin/config.ts +++ b/x-pack/test/licensing_plugin/config.ts @@ -43,7 +43,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...functionalTestsConfig.get('kbnTestServer'), serverArgs: [ ...functionalTestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.licensing.pollingFrequency=300', + '--xpack.licensing.api_polling_frequency=300', ], }, diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js index 80ef6bd6df4ff1..95958d12a42d7d 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.js @@ -16,7 +16,7 @@ export default function ({ getService }) { describe('OpenID Connect authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -46,7 +46,8 @@ export default function ({ getService }) { }); it('should properly set cookie, return all parameters and redirect user for Third Party initiated', async () => { - const handshakeResponse = await supertest.get('/api/security/v1/oidc?iss=https://test-op.elastic.co') + const handshakeResponse = await supertest.post('/api/security/oidc/initiate_login') + .send({ iss: 'https://test-op.elastic.co' }) .expect(302); const cookies = handshakeResponse.headers['set-cookie']; @@ -74,7 +75,7 @@ export default function ({ getService }) { const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0]); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); @@ -108,20 +109,20 @@ export default function ({ getService }) { }); it('should fail if OpenID Connect response is not complemented with handshake cookie', async () => { - await supertest.get(`/api/security/v1/oidc?code=thisisthecode&state=${stateAndNonce.state}`) + await supertest.get(`/api/security/oidc?code=thisisthecode&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .expect(401); }); it('should fail if state is not matching', async () => { - await supertest.get(`/api/security/v1/oidc?code=thisisthecode&state=someothervalue`) + await supertest.get(`/api/security/oidc?code=thisisthecode&state=someothervalue`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); }); it('should succeed if both the OpenID Connect response and the cookie are provided', async () => { - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -139,7 +140,7 @@ export default function ({ getService }) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -160,7 +161,7 @@ export default function ({ getService }) { describe('Complete third party initiated authentication', () => { it('should authenticate a user when a third party initiates the authentication', async () => { - const handshakeResponse = await supertest.get('/api/security/v1/oidc?iss=https://test-op.elastic.co') + const handshakeResponse = await supertest.get('/api/security/oidc/initiate_login?iss=https://test-op.elastic.co') .expect(302); const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0]); const stateAndNonce = getStateAndNonce(handshakeResponse.headers.location); @@ -172,7 +173,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code2&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code2&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -186,7 +187,7 @@ export default function ({ getService }) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -222,7 +223,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -232,7 +233,7 @@ export default function ({ getService }) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -244,7 +245,7 @@ export default function ({ getService }) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -258,7 +259,7 @@ export default function ({ getService }) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -269,7 +270,7 @@ export default function ({ getService }) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -295,7 +296,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -307,7 +308,7 @@ export default function ({ getService }) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); @@ -315,7 +316,7 @@ export default function ({ getService }) { }); it('should redirect to the OPs endsession endpoint to complete logout', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout') + const logoutResponse = await supertest.get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -336,7 +337,7 @@ export default function ({ getService }) { // Tokens that were stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -349,7 +350,7 @@ export default function ({ getService }) { }); it('should reject AJAX requests', async () => { - const ajaxResponse = await supertest.get('/api/security/v1/logout') + const ajaxResponse = await supertest.get('/api/security/logout') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -379,7 +380,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); @@ -408,7 +409,7 @@ export default function ({ getService }) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -422,7 +423,7 @@ export default function ({ getService }) { // Request with old cookie should reuse the same refresh token if within 60 seconds. // Returned cookie will contain the same new access and refresh token pairs as the first request const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -437,14 +438,14 @@ export default function ({ getService }) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', secondNewCookie.cookieString()) .expect(200); @@ -467,7 +468,7 @@ export default function ({ getService }) { .send({ nonce: stateAndNonce.nonce }) .expect(200); - const oidcAuthenticationResponse = await supertest.get(`/api/security/v1/oidc?code=code1&state=${stateAndNonce.state}`) + const oidcAuthenticationResponse = await supertest.get(`/api/security/oidc?code=code1&state=${stateAndNonce.state}`) .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(302); diff --git a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts index 23cbb312b092a7..0e07f01776713d 100644 --- a/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/implicit_flow/oidc_auth.ts @@ -31,7 +31,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should return an HTML page that will parse URL fragment', async () => { - const response = await supertest.get('/api/security/v1/oidc/implicit').expect(200); + const response = await supertest.get('/api/security/oidc/implicit').expect(200); const dom = new JSDOM(response.text, { url: formatURL({ ...config.get('servers.kibana'), auth: false }), runScripts: 'dangerously', @@ -44,7 +44,7 @@ export default function({ getService }: FtrProviderContext) { Object.defineProperty(window, 'location', { value: { href: - 'https://kibana.com/api/security/v1/oidc/implicit#token=some_token&access_token=some_access_token', + 'https://kibana.com/api/security/oidc/implicit#token=some_token&access_token=some_access_token', replace(newLocation: string) { this.href = newLocation; resolve(); @@ -66,17 +66,17 @@ export default function({ getService }: FtrProviderContext) { // Check that script that forwards URL fragment worked correctly. expect(dom.window.location.href).to.be( - '/api/security/v1/oidc?authenticationResponseURI=https%3A%2F%2Fkibana.com%2Fapi%2Fsecurity%2Fv1%2Foidc%2Fimplicit%23token%3Dsome_token%26access_token%3Dsome_access_token' + '/api/security/oidc?authenticationResponseURI=https%3A%2F%2Fkibana.com%2Fapi%2Fsecurity%2Foidc%2Fimplicit%23token%3Dsome_token%26access_token%3Dsome_access_token' ); }); it('should fail if OpenID Connect response is not complemented with handshake cookie', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -86,11 +86,11 @@ export default function({ getService }: FtrProviderContext) { it('should fail if state is not matching', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=$someothervalue&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=$someothervalue&token_type=bearer&access_token=${accessToken}`; await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -102,11 +102,11 @@ export default function({ getService }: FtrProviderContext) { // FLAKY: https://github.com/elastic/kibana/issues/43938 it.skip('should succeed if both the OpenID Connect response and the cookie are provided', async () => { const { idToken, accessToken } = createTokens('1', stateAndNonce.nonce); - const authenticationResponse = `https://kibana.com/api/security/v1/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; + const authenticationResponse = `https://kibana.com/api/security/oidc/implicit#id_token=${idToken}&state=${stateAndNonce.state}&token_type=bearer&access_token=${accessToken}`; const oidcAuthenticationResponse = await supertest .get( - `/api/security/v1/oidc?authenticationResponseURI=${encodeURIComponent( + `/api/security/oidc?authenticationResponseURI=${encodeURIComponent( authenticationResponse )}` ) @@ -129,7 +129,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts index f40db4ccbba0ac..184ccbcdfa6918 100644 --- a/x-pack/test/oidc_api_integration/config.ts +++ b/x-pack/test/oidc_api_integration/config.ts @@ -32,7 +32,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `xpack.security.authc.realms.oidc.oidc1.rp.client_id=0oa8sqpov3TxMWJOt356`, `xpack.security.authc.realms.oidc.oidc1.rp.client_secret=0oa8sqpov3TxMWJOt356`, `xpack.security.authc.realms.oidc.oidc1.rp.response_type=code`, - `xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=http://localhost:${kibanaPort}/api/security/v1/oidc`, + `xpack.security.authc.realms.oidc.oidc1.rp.redirect_uri=http://localhost:${kibanaPort}/api/security/oidc`, `xpack.security.authc.realms.oidc.oidc1.op.authorization_endpoint=https://test-op.elastic.co/oauth2/v1/authorize`, `xpack.security.authc.realms.oidc.oidc1.op.endsession_endpoint=https://test-op.elastic.co/oauth2/v1/endsession`, `xpack.security.authc.realms.oidc.oidc1.op.token_endpoint=http://localhost:${kibanaPort}/api/oidc_provider/token_endpoint`, @@ -52,7 +52,7 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { '--xpack.security.authc.oidc.realm="oidc1"', '--server.xsrf.whitelist', JSON.stringify([ - '/api/security/v1/oidc', + '/api/security/oidc/initiate_login', '/api/oidc_provider/token_endpoint', '/api/oidc_provider/userinfo_endpoint', ]), diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index afb27168d6d5c3..4eee900e68bec9 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -57,7 +57,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject API requests that use untrusted certificate', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -67,7 +67,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -81,7 +81,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(cookie); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -94,7 +94,7 @@ export default function({ getService }: FtrProviderContext) { it('should properly set cookie and authenticate user', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -122,7 +122,7 @@ export default function({ getService }: FtrProviderContext) { // Cookie should be accepted. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -131,7 +131,7 @@ export default function({ getService }: FtrProviderContext) { it('should update session if new certificate is provided', async () => { let response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -143,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(SECOND_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -167,7 +167,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject valid cookie if used with untrusted certificate', async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -179,7 +179,7 @@ export default function({ getService }: FtrProviderContext) { checkCookieIsSet(sessionCookie); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(UNTRUSTED_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -191,7 +191,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -205,7 +205,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -219,7 +219,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -235,7 +235,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -248,7 +248,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') @@ -264,7 +264,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to `logged_out` page after successful logout', async () => { // First authenticate user to retrieve session cookie. const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -277,7 +277,7 @@ export default function({ getService }: FtrProviderContext) { // And then log user out. const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('Cookie', sessionCookie.cookieString()) @@ -292,7 +292,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to home page if session cookie is not provided', async () => { const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(302); @@ -307,7 +307,7 @@ export default function({ getService }: FtrProviderContext) { beforeEach(async () => { const response = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .expect(200); @@ -329,7 +329,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access token. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .ca(CA_CERT) .pfx(FIRST_CLIENT_CERT) .set('kbn-xsrf', 'xxx') diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3815788aa746ed..0436d59906ea87 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -42,7 +42,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookie.httpOnly).to.be(true); const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -64,7 +64,7 @@ export default function({ getService }: FtrProviderContext) { describe('SAML authentication', () => { it('should reject API requests if client is not authenticated', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .expect(401); }); @@ -72,7 +72,7 @@ export default function({ getService }: FtrProviderContext) { it('does not prevent basic login', async () => { const [username, password] = config.get('servers.elasticsearch.auth').split(':'); const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'xxx') .send({ username, password }) .expect(204); @@ -81,7 +81,7 @@ export default function({ getService }: FtrProviderContext) { expect(cookies).to.have.length(1); const { body: user } = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', request.cookie(cookies[0])!.cookieString()) .expect(200); @@ -192,7 +192,7 @@ export default function({ getService }: FtrProviderContext) { const handshakeCookie = request.cookie(handshakeResponse.headers['set-cookie'][0])!; await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', handshakeCookie.cookieString()) .expect(401); @@ -300,7 +300,7 @@ export default function({ getService }: FtrProviderContext) { it('should extend cookie on every successful non-system API call', async () => { const apiResponseOne = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -312,7 +312,7 @@ export default function({ getService }: FtrProviderContext) { expect(sessionCookieOne.value).to.not.equal(sessionCookie.value); const apiResponseTwo = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -326,7 +326,7 @@ export default function({ getService }: FtrProviderContext) { it('should not extend cookie for system API calls', async () => { const systemAPIResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('kbn-system-api', 'true') .set('Cookie', sessionCookie.cookieString()) @@ -337,7 +337,7 @@ export default function({ getService }: FtrProviderContext) { it('should fail and preserve session cookie if unsupported authentication schema is used', async () => { const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Authorization', 'Basic AbCdEf') .set('Cookie', sessionCookie.cookieString()) @@ -383,7 +383,7 @@ export default function({ getService }: FtrProviderContext) { it('should redirect to IdP with SAML request to complete logout', async () => { const logoutResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -404,7 +404,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens that were stored in the previous cookie should be invalidated as well and old // session cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -417,7 +417,7 @@ export default function({ getService }: FtrProviderContext) { }); it('should redirect to home page if session cookie is not provided', async () => { - const logoutResponse = await supertest.get('/api/security/v1/logout').expect(302); + const logoutResponse = await supertest.get('/api/security/logout').expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); expect(logoutResponse.headers.location).to.be('/'); @@ -425,7 +425,7 @@ export default function({ getService }: FtrProviderContext) { it('should reject AJAX requests', async () => { const ajaxResponse = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -441,7 +441,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/v1/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) .set('Cookie', sessionCookie.cookieString()) .expect(302); @@ -462,7 +462,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens that were stored in the previous cookie should be invalidated as well and old session // cookie should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -477,7 +477,7 @@ export default function({ getService }: FtrProviderContext) { it('should invalidate access token on IdP initiated logout even if there is no Kibana session', async () => { const logoutRequest = await createLogoutRequest({ sessionIndex: idpSessionIndex }); const logoutResponse = await supertest - .get(`/api/security/v1/logout?${querystring.stringify(logoutRequest)}`) + .get(`/api/security/logout?${querystring.stringify(logoutRequest)}`) .expect(302); expect(logoutResponse.headers['set-cookie']).to.be(undefined); @@ -490,7 +490,7 @@ export default function({ getService }: FtrProviderContext) { // IdP session id (encoded in SAML LogoutRequest) even if Kibana doesn't provide them and session // cookie with these tokens should not allow API access. const apiResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(400); @@ -548,7 +548,7 @@ export default function({ getService }: FtrProviderContext) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -562,7 +562,7 @@ export default function({ getService }: FtrProviderContext) { // Request with old cookie should reuse the same refresh token if within 60 seconds. // Returned cookie will contain the same new access and refresh token pairs as the first request const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', sessionCookie.cookieString()) .expect(200); @@ -577,14 +577,14 @@ export default function({ getService }: FtrProviderContext) { // The first new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie with fresh pair of access and refresh tokens should work. await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', secondNewCookie.cookieString()) .expect(200); @@ -701,7 +701,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens from old cookie are invalidated. const rejectedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', existingSessionCookie.cookieString()) .expect(400); @@ -712,7 +712,7 @@ export default function({ getService }: FtrProviderContext) { // Only tokens from new session are valid. const acceptedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); @@ -737,7 +737,7 @@ export default function({ getService }: FtrProviderContext) { // Tokens from old cookie are invalidated. const rejectedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', existingSessionCookie.cookieString()) .expect(400); @@ -748,7 +748,7 @@ export default function({ getService }: FtrProviderContext) { // Only tokens from new session are valid. const acceptedResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'xxx') .set('Cookie', newSessionCookie.cookieString()) .expect(200); diff --git a/x-pack/test/saved_object_api_integration/common/services/legacy_es.js b/x-pack/test/saved_object_api_integration/common/services/legacy_es.js index 94aa6025aa6994..9267fa312ed065 100644 --- a/x-pack/test/saved_object_api_integration/common/services/legacy_es.js +++ b/x-pack/test/saved_object_api_integration/common/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../../plugins/security/server/elasticsearch_client_plugin'; export function LegacyEsProvider({ getService }) { const config = getService('config'); @@ -16,6 +16,6 @@ export function LegacyEsProvider({ getService }) { return new legacyElasticsearch.Client({ host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin], + plugins: [elasticsearchClientPlugin], }); } diff --git a/x-pack/test/spaces_api_integration/common/services/legacy_es.js b/x-pack/test/spaces_api_integration/common/services/legacy_es.js index 5e8137f0d11b5f..5862fe877ba5c3 100644 --- a/x-pack/test/spaces_api_integration/common/services/legacy_es.js +++ b/x-pack/test/spaces_api_integration/common/services/legacy_es.js @@ -8,7 +8,7 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; -import shieldPlugin from '../../../../legacy/server/lib/esjs_shield_plugin'; +import { elasticsearchClientPlugin } from '../../../../plugins/security/server/elasticsearch_client_plugin'; export function LegacyEsProvider({ getService }) { const config = getService('config'); @@ -16,6 +16,6 @@ export function LegacyEsProvider({ getService }) { return new legacyElasticsearch.Client({ host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [shieldPlugin] + plugins: [elasticsearchClientPlugin] }); } diff --git a/x-pack/test/token_api_integration/auth/header.js b/x-pack/test/token_api_integration/auth/header.js index 4b27fd5db31663..1c88f28a655417 100644 --- a/x-pack/test/token_api_integration/auth/header.js +++ b/x-pack/test/token_api_integration/auth/header.js @@ -25,7 +25,7 @@ export default function ({ getService }) { const token = await createToken(); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); @@ -36,14 +36,14 @@ export default function ({ getService }) { // try it once await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); // try it again to verity it isn't invalidated after a single request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(200); @@ -51,7 +51,7 @@ export default function ({ getService }) { it('rejects invalid access token via authorization Bearer header', async () => { await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', 'Bearer notreal') .expect(401); @@ -67,7 +67,7 @@ export default function ({ getService }) { await new Promise(resolve => setTimeout(() => resolve(), 20000)); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('authorization', `Bearer ${token}`) .expect(401); diff --git a/x-pack/test/token_api_integration/auth/login.js b/x-pack/test/token_api_integration/auth/login.js index 2e6a2e2f81e4ff..aba7e3852aa1f8 100644 --- a/x-pack/test/token_api_integration/auth/login.js +++ b/x-pack/test/token_api_integration/auth/login.js @@ -17,7 +17,7 @@ export default function ({ getService }) { describe('login', () => { it('accepts valid login credentials as 204 status', async () => { await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }) .expect(204); @@ -25,7 +25,7 @@ export default function ({ getService }) { it('sets HttpOnly cookie with valid login', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }) .expect(204); @@ -42,7 +42,7 @@ export default function ({ getService }) { it('rejects without kbn-xsrf header as 400 status even if credentials are valid', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .send({ username: 'elastic', password: 'changeme' }) .expect(400); @@ -53,7 +53,7 @@ export default function ({ getService }) { it('rejects without credentials as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .expect(400); @@ -64,7 +64,7 @@ export default function ({ getService }) { it('rejects without password as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic' }) .expect(400); @@ -76,7 +76,7 @@ export default function ({ getService }) { it('rejects without username as 400 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ password: 'changme' }) .expect(400); @@ -88,7 +88,7 @@ export default function ({ getService }) { it('rejects invalid credentials as 401 status', async () => { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'notvalidpassword' }) .expect(401); diff --git a/x-pack/test/token_api_integration/auth/logout.js b/x-pack/test/token_api_integration/auth/logout.js index 90634886819584..fa7a0606c3770f 100644 --- a/x-pack/test/token_api_integration/auth/logout.js +++ b/x-pack/test/token_api_integration/auth/logout.js @@ -16,7 +16,7 @@ export default function ({ getService }) { async function createSessionCookie() { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }); @@ -33,7 +33,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()) .expect(302) .expect('location', '/login?msg=LOGGED_OUT'); @@ -43,7 +43,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); const response = await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()); const newCookie = extractSessionCookie(response); @@ -60,12 +60,12 @@ export default function ({ getService }) { // destroy it await supertest - .get('/api/security/v1/logout') + .get('/api/security/logout') .set('cookie', cookie.cookieString()); // verify that the cookie no longer works await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(400); diff --git a/x-pack/test/token_api_integration/auth/session.js b/x-pack/test/token_api_integration/auth/session.js index 8a9f1d7a3f2295..6e8e8c01f3da6b 100644 --- a/x-pack/test/token_api_integration/auth/session.js +++ b/x-pack/test/token_api_integration/auth/session.js @@ -19,7 +19,7 @@ export default function ({ getService }) { async function createSessionCookie() { const response = await supertest - .post('/api/security/v1/login') + .post('/internal/security/login') .set('kbn-xsrf', 'true') .send({ username: 'elastic', password: 'changeme' }); @@ -36,7 +36,7 @@ export default function ({ getService }) { const cookie = await createSessionCookie(); await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); @@ -47,14 +47,14 @@ export default function ({ getService }) { // try it once await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); // try it again to verity it isn't invalidated after a single request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', cookie.cookieString()) .expect(200); @@ -85,7 +85,7 @@ export default function ({ getService }) { // This api call should succeed and automatically refresh token. Returned cookie will contain // the new access and refresh token pair. const firstResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('cookie', originalCookie.cookieString()) .expect(200); @@ -96,7 +96,7 @@ export default function ({ getService }) { // Request with old cookie should return another valid cookie we can use to authenticate requests // if it happens within 60 seconds of the refresh token being used const secondResponse = await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', originalCookie.cookieString()) .expect(200); @@ -110,14 +110,14 @@ export default function ({ getService }) { // The first new cookie should authenticate a subsequent request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', firstNewCookie.cookieString()) .expect(200); // The second new cookie should authenticate a subsequent request await supertest - .get('/api/security/v1/me') + .get('/internal/security/me') .set('kbn-xsrf', 'true') .set('Cookie', secondNewCookie.cookieString()) .expect(200); diff --git a/x-pack/test/typings/hapi.d.ts b/x-pack/test/typings/hapi.d.ts index 6a67c5ccee337b..fa2712e69a5b0b 100644 --- a/x-pack/test/typings/hapi.d.ts +++ b/x-pack/test/typings/hapi.d.ts @@ -6,7 +6,7 @@ import 'hapi'; -import { XPackMainPlugin } from '../../legacy/plugins/xpack_main/xpack_main'; +import { XPackMainPlugin } from '../../legacy/plugins/xpack_main/server/xpack_main'; import { SecurityPlugin } from '../../legacy/plugins/security'; import { ActionsPlugin, ActionsClient } from '../../legacy/plugins/actions'; import { TaskManager } from '../../legacy/plugins/task_manager'; diff --git a/x-pack/test/visual_regression/tests/login_page.js b/x-pack/test/visual_regression/tests/login_page.js index 003a23086c7b6c..04f6dff5fcaece 100644 --- a/x-pack/test/visual_regression/tests/login_page.js +++ b/x-pack/test/visual_regression/tests/login_page.js @@ -15,7 +15,7 @@ export default function ({ getService, getPageObjects }) { describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { @@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }) { }); afterEach(async () => { - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); it('renders login page', async () => { diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index d97e47f3ce0f8a..569508caf3f208 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -6,7 +6,7 @@ import 'hapi'; -import { XPackMainPlugin } from '../legacy/plugins/xpack_main/xpack_main'; +import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; import { SecurityPlugin } from '../legacy/plugins/security'; import { ActionsPlugin, ActionsClient } from '../legacy/plugins/actions'; import { TaskManager } from '../legacy/plugins/task_manager'; diff --git a/yarn.lock b/yarn.lock index cfef1bec7e6a05..a67fcda4febcab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/cli@7.5.5", "@babel/cli@^7.5.5": +"@babel/cli@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/cli/-/cli-7.5.5.tgz#bdb6d9169e93e241a08f5f7b0265195bf38ef5ec" integrity sha512-UHI+7pHv/tk9g6WXQKYz+kmXTI77YtuY3vqC59KIqcoWEjsJJSG6rAxKaLsgj3LDyadsPrCB929gVOKM6Hui0w== @@ -1005,24 +1005,17 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@7.5.5", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5": +"@babel/runtime@7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.4.4", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": - version "7.7.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a" - integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw== - dependencies: - regenerator-runtime "^0.13.2" - -"@babel/runtime@^7.5.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2": - version "7.7.4" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.4.tgz#b23a856751e4bf099262f867767889c0e3fe175b" - integrity sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2": + version "7.7.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f" + integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw== dependencies: regenerator-runtime "^0.13.2" @@ -3427,6 +3420,14 @@ resolved "https://registry.yarnpkg.com/@types/hoek/-/hoek-4.1.3.tgz#d1982d48fb0d2a0e5d7e9d91838264d8e428d337" integrity sha1-0ZgtSPsNKg5dfp2Rg4Jk2OQo0zc= +"@types/hoist-non-react-statics@*": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/indent-string@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/indent-string/-/indent-string-3.0.0.tgz#9ebb391ceda548926f5819ad16405349641b999f" @@ -3859,19 +3860,19 @@ dependencies: "@types/react" "*" -"@types/react-router-dom@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04" - integrity sha512-GbztJAScOmQ/7RsQfO4cd55RuH1W4g6V1gDW3j4riLlt+8yxYLqqsiMzmyuXBLzdFmDtX/uU2Bpcm0cmudv44A== +"@types/react-router-dom@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" + integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA== dependencies: "@types/history" "*" "@types/react" "*" "@types/react-router" "*" -"@types/react-router@*": - version "4.0.32" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.32.tgz#501529e3d7aa7d5c738d339367e1a7dd5338b2a7" - integrity sha512-VLQSifCIKCTpfMFrJN/nO5a45LduB6qSMkO9ASbcGdCHiDwJnrLNzk91Q895yG0qWY7RqT2jR16giBRpRG1HQw== +"@types/react-router@*", "@types/react-router@^5.1.3": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.3.tgz#7c7ca717399af64d8733d8cb338dd43641b96f2d" + integrity sha512-0gGhmerBqN8CzlnDmSgGNun3tuZFXerUclWkqEhozdLaJtfcJRUTGkKaEKk+/MpHd1KDS1+o2zb/3PkBUiv2qQ== dependencies: "@types/history" "*" "@types/react" "*" @@ -3968,10 +3969,10 @@ "@types/tough-cookie" "*" form-data "^2.5.0" -"@types/selenium-webdriver@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.3.tgz#388f12c464cc1fff5d4c84cb372f19b9ab9b5c81" - integrity sha512-aMKIG1IKwV9/gjhm9uICjvmy4s2SL/bF9fE2WEgLhfdrTLKSIsDMt9M2pTqhZlxllgQPa+EUddtkx4YFTSjadw== +"@types/selenium-webdriver@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.0.5.tgz#23041a4948c82daf2df9836e4d2358fec10d3e24" + integrity sha512-ma1aL1znI3ptEbSQgbywgadrRCJouPIACSfOl/bPwu/TPNSyyE/+o9jZ6+bpDVTtIdksZuVKpq4SR1ip3DRduw== "@types/semver@^5.5.0": version "5.5.0" @@ -4019,11 +4020,12 @@ dependencies: "@types/node" "*" -"@types/styled-components@^4.4.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.0.tgz#15a3d59533fd3a5bd013db4a7c4422ec542c59d2" - integrity sha512-QFl+w3hQJNHE64Or3PXMFpC3HAQDiuQLi5o9m1XPEwYWfgCZtAribO5ksjxnO8U0LG8Parh0ESCgVxo4VfxlHg== +"@types/styled-components@^4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-4.4.1.tgz#bc40cf5ce0708032f4b148b04ab3c470d3e74026" + integrity sha512-cQXT4pkAkM0unk/s26UBrJx9RmJ2rNUn2aDTgzp1rtu+tTkScebE78jbxNWhlqkA43XF3d41CcDlyl9Ldotm2g== dependencies: + "@types/hoist-non-react-statics" "*" "@types/react" "*" "@types/react-native" "*" csstype "^2.2.0" @@ -6191,7 +6193,7 @@ babel-plugin-react-docgen@^3.0.0: resolved "https://registry.yarnpkg.com/babel-plugin-require-context-hook-babel7/-/babel-plugin-require-context-hook-babel7-1.0.0.tgz#1273d4cee7e343d0860966653759a45d727e815d" integrity sha512-kez0BAN/cQoyO1Yu1nre1bQSYZEF93Fg7VQiBHFfMWuaZTy7vJSTT4FY68FwHTYG53Nyt0A7vpSObSVxwweQeQ== -"babel-plugin-styled-components@>= 1": +"babel-plugin-styled-components@>= 1", babel-plugin-styled-components@^1.10.6: version "1.10.6" resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.6.tgz#f8782953751115faf09a9f92431436912c34006b" integrity sha512-gyQj/Zf1kQti66100PhrCRjI5ldjaze9O0M3emXRPAN80Zsf8+e1thpTpaXJXVHXtaM4/+dJEgZHyS9Its+8SA== @@ -7812,10 +7814,10 @@ chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@78.0.1: - version "78.0.1" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-78.0.1.tgz#2db3425a2cba6fcaf1a41d9538b16c3d06fa74a8" - integrity sha512-eOsyFk4xb9EECs1VMrDbxO713qN+Bu1XUE8K9AuePc3839TPdAegg72kpXSzkeNqRNZiHbnJUItIVCLFkDqceA== +chromedriver@79.0.0: + version "79.0.0" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-79.0.0.tgz#1660ac29924dfcd847911025593d6b6746aeea35" + integrity sha512-DO29C7ntJfzu6q1vuoWwCON8E9x5xzopt7Q41A7Dr7hBKcdNpGw1l9DTt9b+l1qviOWiJLGsD+jHw21ptEHubA== dependencies: del "^4.1.1" extract-zip "^1.6.7" @@ -8332,10 +8334,10 @@ commander@2.17.x, commander@~2.17.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@3.0.0, commander@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" - integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== +commander@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== commander@^2.13.0, commander@^2.15.1, commander@^2.16.0, commander@^2.19.0, commander@^2.20.0, commander@^2.7.1: version "2.20.0" @@ -8352,6 +8354,11 @@ commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" integrity sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ== +commander@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.0.tgz#0641ea00838c7a964627f04cddc336a2deddd60a" + integrity sha512-pl3QrGOBa9RZaslQiqnnKX2J068wcQw7j9AIaBQ9/JEp5RY6je4jKTImg0Bd+rpoONSe7GUFSgkxLeo17m3Pow== + commander@~2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" @@ -14855,17 +14862,6 @@ history@^3.0.0: query-string "^4.2.2" warning "^3.0.0" -history@^4.7.2: - version "4.7.2" - resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" - integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA== - dependencies: - invariant "^2.2.1" - loose-envify "^1.2.0" - resolve-pathname "^2.2.0" - value-equal "^0.4.0" - warning "^3.0.0" - hjson@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/hjson/-/hjson-3.2.0.tgz#76203ea69bc1c7c88422b48402cc34df8ff8de0e" @@ -14912,6 +14908,13 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: dependencies: react-is "^16.7.0" +hoist-non-react-statics@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#101685d3aff3b23ea213163f6e8e12f4f111e19f" + integrity sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw== + dependencies: + react-is "^16.7.0" + homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" @@ -19395,6 +19398,15 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY= +mini-create-react-context@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189" + integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw== + dependencies: + "@babel/runtime" "^7.4.0" + gud "^1.0.0" + tiny-warning "^1.0.2" + mini-css-extract-plugin@0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1" @@ -22952,10 +22964,10 @@ react-beautiful-dnd@^10.1.0: redux "^4.0.1" tiny-invariant "^1.0.4" -react-beautiful-dnd@^12.1.1: - version "12.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#810f9b9d94f667b15b253793e853d016a0f3f07c" - integrity sha512-w/mpIXMEXowc53PCEnMoFyAEYFgxMfygMK5msLo5ifJ2/CiSACLov9A79EomnPF7zno3N207QGXsraBxAJnyrw== +react-beautiful-dnd@^12.2.0: + version "12.2.0" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" + integrity sha512-s5UrOXNDgeEC+sx65IgbeFlqKKgK3c0UfbrJLWufP34WBheyu5kJ741DtJbsSgPKyNLkqfswpMYr0P8lRj42cA== dependencies: "@babel/runtime-corejs2" "^7.6.3" css-box-model "^1.2.0" @@ -23450,23 +23462,40 @@ react-reverse-portal@^1.0.4: resolved "https://registry.yarnpkg.com/react-reverse-portal/-/react-reverse-portal-1.0.4.tgz#d127d2c9147549b25c4959aba1802eca4b144cd4" integrity sha512-WESex/wSjxHwdG7M0uwPNkdQXaLauXNHi4INQiRybmFIXVzAqgf/Ak2OzJ4MLf4UuCD/IzEwJOkML2SxnnontA== -react-router-dom@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" - integrity sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== +react-router-dom@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18" + integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew== dependencies: - history "^4.7.2" - invariant "^2.2.4" + "@babel/runtime" "^7.1.2" + history "^4.9.0" loose-envify "^1.3.1" - prop-types "^15.6.1" - react-router "^4.3.1" - warning "^4.0.1" + prop-types "^15.6.2" + react-router "5.1.2" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" react-router-redux@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e" integrity sha1-InQDWWtRUeGCN32rg1tdRfD4BU4= +react-router@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418" + integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A== + dependencies: + "@babel/runtime" "^7.1.2" + history "^4.9.0" + hoist-non-react-statics "^3.1.0" + loose-envify "^1.3.1" + mini-create-react-context "^0.3.0" + path-to-regexp "^1.7.0" + prop-types "^15.6.2" + react-is "^16.6.0" + tiny-invariant "^1.0.2" + tiny-warning "^1.0.0" + react-router@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.1.tgz#b9a3279962bdfbe684c8bd0482b81ef288f0f244" @@ -23480,19 +23509,6 @@ react-router@^3.2.0: prop-types "^15.5.6" warning "^3.0.0" -react-router@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" - integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== - dependencies: - history "^4.7.2" - hoist-non-react-statics "^2.5.0" - invariant "^2.2.4" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.1" - warning "^4.0.1" - react-select@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.0.8.tgz#06ff764e29db843bcec439ef13e196865242e0c1" @@ -27516,6 +27532,11 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" integrity sha512-rru86D9CpQRLvsFG5XFdy0KdLAvjdQDyZCsRcuu60WtzFylDM3eAWSxEVz5kzL2Gp544XiUvPbVKtOA/txLi9Q== +tiny-warning@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinycolor2@1.4.1, tinycolor2@^1.0.0, tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" @@ -29817,13 +29838,6 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" -warning@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607" - integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== - dependencies: - loose-envify "^1.0.0" - warning@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"