diff --git a/.backportrc.json b/.backportrc.json index 3f1d639e9a480..a97c82ca0efa9 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.10", "7.9", "7.8", "7.7", @@ -27,7 +28,7 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.10.0$": "7.x", + "^v7.11.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" } } diff --git a/.eslintrc.js b/.eslintrc.js index a0363e77e3596..27dacd51be6f2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1046,9 +1046,7 @@ module.exports = { */ { // typescript only for front and back end - files: [ - 'x-pack/plugins/{alerts,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', - ], + files: ['x-pack/plugins/{alerts,stack_alerts,actions,task_manager,event_log}/**/*.{ts,tsx}'], rules: { '@typescript-eslint/no-explicit-any': 'error', }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d1cf0300b9e17..5dd41581914ed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -137,6 +137,7 @@ /src/plugins/home/public @elastic/kibana-core-ui /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui +/src/plugins/kibana_overview/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui #CC# /src/legacy/core_plugins/newsfeed @elastic/kibana-core-ui #CC# /src/legacy/server/sample_data/ @elastic/kibana-core-ui @@ -255,6 +256,8 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform +/src/plugins/security_oss/ @elastic/kibana-security +/test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security @@ -263,10 +266,8 @@ /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security /x-pack/test/kerberos_api_integration/ @elastic/kibana-security -/x-pack/test/login_selector_api_integration/ @elastic/kibana-security /x-pack/test/oidc_api_integration/ @elastic/kibana-security /x-pack/test/pki_api_integration/ @elastic/kibana-security -/x-pack/test/saml_api_integration/ @elastic/kibana-security /x-pack/test/security_api_integration/ @elastic/kibana-security /x-pack/test/security_functional/ @elastic/kibana-security /x-pack/test/spaces_api_integration/ @elastic/kibana-security @@ -316,7 +317,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib #CC# /x-pack/legacy/plugins/alerting/ @elastic/kibana-alerting-services #CC# /x-pack/legacy/plugins/task_manager @elastic/kibana-alerting-services #CC# /x-pack/legacy/plugins/triggers_actions_ui/ @elastic/kibana-alerting-services -#CC# /x-pack/plugins/alerting_builtins @elastic/kibana-alerting-services +#CC# /x-pack/plugins/stack_alerts @elastic/kibana-alerting-services # Enterprise Search # Shared diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 534b1cea6242f..c366819c49dde 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,8 +9,9 @@ Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios -- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist) -- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server) +- [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) +- [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) +- [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### For maintainers diff --git a/.i18nrc.json b/.i18nrc.json index 153a5a6cafece..e0281b0a5bc21 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -29,6 +29,7 @@ "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", + "kibanaOverview": "src/plugins/kibana_overview", "kibana_react": "src/legacy/core_plugins/kibana_react", "kibana-react": "src/plugins/kibana_react", "kibana_utils": "src/plugins/kibana_utils", @@ -37,6 +38,7 @@ "regionMap": "src/plugins/region_map", "savedObjects": "src/plugins/saved_objects", "savedObjectsManagement": "src/plugins/saved_objects_management", + "security": "src/plugins/security_oss", "server": "src/legacy/server", "statusPage": "src/legacy/core_plugins/status_page", "telemetry": [ diff --git a/docs/api/saved-objects/bulk_create.asciidoc b/docs/api/saved-objects/bulk_create.asciidoc index 5149cef3d30c6..267ab3891d700 100644 --- a/docs/api/saved-objects/bulk_create.asciidoc +++ b/docs/api/saved-objects/bulk_create.asciidoc @@ -41,9 +41,10 @@ experimental[] Create multiple {kib} saved objects. `references`:: (Optional, array) Objects with `name`, `id`, and `type` properties that describe the other saved objects in the referenced object. To refer to the other saved object, use `name` in the attributes. Never use `id` to refer to the other saved object. `id` can be automatically updated during migrations, import, or export. -`namespaces`:: - (Optional, string array) Identifiers for the <> in which this object should be created. If this is not provided, the - object will be created in the current space. +`initialNamespaces`:: + (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the + object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space + (default behavior). `version`:: (Optional, number) Specifies the version. diff --git a/docs/api/saved-objects/create.asciidoc b/docs/api/saved-objects/create.asciidoc index c8cd9c8bfca27..50809a1bd5d4e 100644 --- a/docs/api/saved-objects/create.asciidoc +++ b/docs/api/saved-objects/create.asciidoc @@ -46,9 +46,10 @@ any data that you send to the API is properly formed. `references`:: (Optional, array) Objects with `name`, `id`, and `type` properties that describe the other saved objects that this object references. Use `name` in attributes to refer to the other saved object, but never the `id`, which can update automatically during migrations or import/export. -`namespaces`:: - (Optional, string array) Identifiers for the <> in which this object should be created. If this is not provided, the - object will be created in the current space. +`initialNamespaces`:: + (Optional, string array) Identifiers for the <> in which this object is created. If this is provided, the + object is created only in the explicitly defined spaces. If this is not provided, the object is created in the current space + (default behavior). [[saved-objects-api-create-request-codes]] ==== Response code diff --git a/docs/developer/best-practices/index.asciidoc b/docs/developer/best-practices/index.asciidoc index 13ea010d0aa96..42b379e606898 100644 --- a/docs/developer/best-practices/index.asciidoc +++ b/docs/developer/best-practices/index.asciidoc @@ -138,9 +138,12 @@ Review: * <> * <> * <> +* <> include::navigation.asciidoc[leveloffset=+1] include::stability.asciidoc[leveloffset=+1] include::security.asciidoc[leveloffset=+1] + +include::typescript.asciidoc[leveloffset=+1] diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc new file mode 100644 index 0000000000000..3321aae3c0994 --- /dev/null +++ b/docs/developer/best-practices/typescript.asciidoc @@ -0,0 +1,64 @@ +[[typescript]] +== Typescript + +Although this is not a requirement, we encourage if all new code is developed in https://www.typescriptlang.org/[Typescript]. + +[discrete] +=== Project references +Kibana has crossed the 2m LoC mark. The current situation creates some scaling problems when the default out-of-the-box setup stops working. As a result, developers suffer from slow project compilation and IDE unresponsiveness. As a part of https://github.com/elastic/kibana/projects/63[Developer Experience project], we are migrating our tooling to use built-in TypeScript features addressing the scaling problems - https://www.typescriptlang.org/docs/handbook/project-references.html[project references] & https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#faster-subsequent-builds-with-the---incremental-flag[incremental builds] + +In a nutshell - instead of compiling the whole Kibana codebase at once, this setup enforces splitting the code base into independent projects that form a directed acyclic graph (DAG). This allows the TypeScript compiler (`tsc`) to apply several advanced optimizations: + +- Every project emits `public` interfaces in the form of `d.ts` type declarations generated by the TypeScript compiler +- These generated `d.ts` type declarations are used whenever a referenced project is imported in a depending project +- This makes it possible to determine which project needs rebuilding when the source code has changed to use a more aggressive caching strategy. + +More details are available in the https://www.typescriptlang.org/docs/handbook/project-references.html[official docs] + +[discrete] +==== Caveats +This architecture imposes several limitations to which we must comply: + +- Projects cannot have circular dependencies. Even though the Kibana platform doesn't support circular dependencies between Kibana plugins, TypeScript (and ES6 modules) does allow circular imports between files. So in theory, you may face a problem when migrating to the TS project references and you will have to resolve this circular dependency. +- A project must emit its type declaration. It's not always possible to generate a type declaration if the compiler cannot infer a type. There are two basic cases: + +1. Your plugin exports a type inferring an internal type declared in Kibana codebase. In this case, you'll have to either export an internal type or to declare an exported type explicitly. +2. Your plugin exports something inferring a type from a 3rd party library that doesn't export this type. To fix the problem, you have to declare the exported type manually. + +[discrete] +==== Prerequisites +Since `tsc` doesn't support circular project references, the migration order does matter. You can migrate your plugin only when all the plugin dependencies already have migrated. It creates a situation where commonly used plugins (such as `data` or `kibana_react`) have to migrate first. + +[discrete] +==== Implementation +- Make sure all the plugins listed as dependencies in `kibana.json` file have migrated to TS project references. +- Add `tsconfig.json` in the root folder of your plugin. +[source,json] +---- +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders containg files to be compiled + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + // add references to other TypeScript projects your plugin dependes on + ] +} +---- +If your plugin imports a file not listed in `include`, the build will fail with the next message `File ‘…’ is not listed within the file list of project …’. Projects must list all files or use an 'include' pattern.` + +- Build you plugin `./node_modules/.bin/tsc -b src/plugins/my_plugin`. Fix errors if `tsc` cannot generate type declarations for your project. +- Add your project reference to `references` property of `tsconfig.refs.json` +- Add your plugin to `references` property and plugin folder to `exclude` property of the `tsconfig.json` it used to belong to (for example, for `src/plugins/**` it's `tsconfig.json`; for `x-pack/plugins/**` it’s `x-pack/tsconfig.json`). +- List the reference to your newly created project in all the Kibana `tsconfig.json` files that could import your project: `tsconfig.json`, `test/tsconfig.json`, `x-pack/tsconfig.json`, `x-pack/test/tsconfig.json`. And in all the plugin-specific `tsconfig.refs.json` for dependent plugins. +- You can measure how your changes affect `tsc` compiler performance with `node --max-old-space-size=4096 ./node_modules/.bin/tsc -p tsconfig.json --extendedDiagnostics --noEmit`. Compare with `master` branch. + +You can use https://github.com/elastic/kibana/pull/79446 as an example. diff --git a/docs/developer/contributing/index.asciidoc b/docs/developer/contributing/index.asciidoc index ecb37ffe9c97b..ba4ab89d17c27 100644 --- a/docs/developer/contributing/index.asciidoc +++ b/docs/developer/contributing/index.asciidoc @@ -49,7 +49,7 @@ The Release Notes summarize what the PRs accomplish in language that is meaningf The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description. -To use a single paragraph of text, enter `Release note:` or a `## Release note` header in the PR description, followed by your text. For example, refer to this https://github.com/elastic/kibana/pull/65796[PR] that uses the `## Release note` header. +To use a single paragraph of text, enter a `Release note:` or `## Release note` header in the PR description ("dev docs" works too), followed by your text. For example, refer to this https://github.com/elastic/kibana/pull/65796[PR] that uses the `## Release note` header. When you create the Release Notes text, use the following best practices: diff --git a/docs/developer/index.asciidoc b/docs/developer/index.asciidoc index 5f032a3952173..9e349a38557f2 100644 --- a/docs/developer/index.asciidoc +++ b/docs/developer/index.asciidoc @@ -1,7 +1,6 @@ [[development]] = Developer guide -[partintro] -- Contributing to {kib} can be daunting at first, but it doesn't have to be. The following sections should get you up and running in no time. If you have any problems, file an issue in the https://github.com/elastic/kibana/issues[Kibana repo]. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 67b7aa8e6a011..21c51f8cabd32 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -110,6 +110,10 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |This plugin contains several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. +|{kib-repo}blob/{branch}/src/plugins/kibana_overview/README.md[kibanaOverview] +|An overview page highlighting Kibana apps + + |{kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] |Tools for building React applications in Kibana. @@ -155,6 +159,11 @@ It also provides a stateful version of it on the start contract. |WARNING: Missing README. +|{kib-repo}blob/{branch}/src/plugins/security_oss/README.md[securityOss] +|securityOss is responsible for educating users about Elastic's free security features, +so they can properly protect the data within their clusters. + + |{kib-repo}blob/{branch}/src/plugins/share/README.md[share] |Replaces the legacy ui/share module for registering share context menus. @@ -269,13 +278,6 @@ which will load the visualization's editor. |The Kibana actions plugin provides a framework to create executable actions. You can: -|{kib-repo}blob/{branch}/x-pack/plugins/alerting_builtins/README.md[alertingBuiltins] -|This plugin provides alertTypes shipped with Kibana for use with the -the alerts plugin. When enabled, it will register -the built-in alertTypes with the alerting plugin, register associated HTTP -routes, etc. - - |{kib-repo}blob/{branch}/x-pack/plugins/alerts/README.md[alerts] |The Kibana alerting plugin provides a common place to set up alerts. You can: @@ -397,7 +399,7 @@ the infrastructure monitoring use-case within Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/ingest_manager/README.md[ingestManager] -|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.ingestManager.fleet.tlsCheckDisabled=false) +|Fleet needs to have Elasticsearch API keys enabled, and also to have TLS enabled on kibana, (if you want to run Kibana without TLS you can provide the following config flag --xpack.fleet.agents.tlsCheckDisabled=false) |{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] @@ -481,6 +483,13 @@ using the CURL scripts in the scripts folder. |WARNING: Missing README. +|{kib-repo}blob/{branch}/x-pack/plugins/stack_alerts/README.md[stackAlerts] +|This plugin provides alertTypes shipped with Kibana for use with the +the alerts plugin. When enabled, it will register +the alertTypes by the Stack in the alerting plugin, register associated HTTP +routes, etc. + + |{kib-repo}blob/{branch}/x-pack/plugins/task_manager[taskManager] |WARNING: Missing README. @@ -497,7 +506,7 @@ using the CURL scripts in the scripts folder. |WARNING: Missing README. -|{kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggers_actions_ui] +|{kib-repo}blob/{branch}/x-pack/plugins/triggers_actions_ui/README.md[triggersActionsUi] |The Kibana alerts and actions UI plugin provides a user interface for managing alerts and actions. As a developer you can reuse and extend built-in alerts and actions UI functionality: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index be8b7c27495ad..a484c856ec015 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -121,6 +121,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-core-server.onpreresponseinfo.md) | Response status code. | +| [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) | Additional data to extend a response when rendering a new body | | [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreResponse interceptor for incoming request. | | [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | | [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.body.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.body.md new file mode 100644 index 0000000000000..ab5b5e7a4f272 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.body.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) > [body](./kibana-plugin-core-server.onpreresponserender.body.md) + +## OnPreResponseRender.body property + +the body to use in the response + +Signature: + +```typescript +body: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.headers.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.headers.md new file mode 100644 index 0000000000000..100d12f63d165 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.headers.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) > [headers](./kibana-plugin-core-server.onpreresponserender.headers.md) + +## OnPreResponseRender.headers property + +additional headers to attach to the response + +Signature: + +```typescript +headers?: ResponseHeaders; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md new file mode 100644 index 0000000000000..0a7ce2d546701 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponserender.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreResponseRender](./kibana-plugin-core-server.onpreresponserender.md) + +## OnPreResponseRender interface + +Additional data to extend a response when rendering a new body + +Signature: + +```typescript +export interface OnPreResponseRender +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [body](./kibana-plugin-core-server.onpreresponserender.body.md) | string | the body to use in the response | +| [headers](./kibana-plugin-core-server.onpreresponserender.headers.md) | ResponseHeaders | additional headers to attach to the response | + diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md index 44da09d0cc68e..14070038132da 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md @@ -17,4 +17,5 @@ export interface OnPreResponseToolkit | Property | Type | Description | | --- | --- | --- | | [next](./kibana-plugin-core-server.onpreresponsetoolkit.next.md) | (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult | To pass request to the next handler | +| [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md) | (responseRender: OnPreResponseRender) => OnPreResponseResult | To override the response with a different body | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.render.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.render.md new file mode 100644 index 0000000000000..7dced7fe8deee --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.render.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) > [render](./kibana-plugin-core-server.onpreresponsetoolkit.render.md) + +## OnPreResponseToolkit.render property + +To override the response with a different body + +Signature: + +```typescript +render: (responseRender: OnPreResponseRender) => OnPreResponseResult; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md similarity index 68% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.namespaces.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md index 7db1c53c67b52..3db8bbadfbd6b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.namespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [namespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.namespaces.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkCreateObject](./kibana-plugin-core-server.savedobjectsbulkcreateobject.md) > [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) -## SavedObjectsBulkCreateObject.namespaces property +## SavedObjectsBulkCreateObject.initialNamespaces property Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). @@ -11,5 +11,5 @@ Note: this can only be used for multi-namespace object types. Signature: ```typescript -namespaces?: string[]; +initialNamespaces?: string[]; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md index aabbfeeff75af..5ac5f6d9807bd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkcreateobject.md @@ -17,8 +17,8 @@ export interface SavedObjectsBulkCreateObject | --- | --- | --- | | [attributes](./kibana-plugin-core-server.savedobjectsbulkcreateobject.attributes.md) | T | | | [id](./kibana-plugin-core-server.savedobjectsbulkcreateobject.id.md) | string | | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [migrationVersion](./kibana-plugin-core-server.savedobjectsbulkcreateobject.migrationversion.md) | SavedObjectsMigrationVersion | 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. | -| [namespaces](./kibana-plugin-core-server.savedobjectsbulkcreateobject.namespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [originId](./kibana-plugin-core-server.savedobjectsbulkcreateobject.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [references](./kibana-plugin-core-server.savedobjectsbulkcreateobject.references.md) | SavedObjectReference[] | | | [type](./kibana-plugin-core-server.savedobjectsbulkcreateobject.type.md) | string | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.namespaces.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md similarity index 69% rename from docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.namespaces.md rename to docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md index 67804999dfd44..262b0997cb905 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.namespaces.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [namespaces](./kibana-plugin-core-server.savedobjectscreateoptions.namespaces.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) > [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) -## SavedObjectsCreateOptions.namespaces property +## SavedObjectsCreateOptions.initialNamespaces property Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md). @@ -11,5 +11,5 @@ Note: this can only be used for multi-namespace object types. Signature: ```typescript -namespaces?: string[]; +initialNamespaces?: string[]; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md index 63aebf6c5e791..e6d306784f8ae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscreateoptions.md @@ -16,8 +16,8 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions | Property | Type | Description | | --- | --- | --- | | [id](./kibana-plugin-core-server.savedobjectscreateoptions.id.md) | string | (not recommended) Specify an id for the document | +| [initialNamespaces](./kibana-plugin-core-server.savedobjectscreateoptions.initialnamespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [migrationVersion](./kibana-plugin-core-server.savedobjectscreateoptions.migrationversion.md) | SavedObjectsMigrationVersion | 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. | -| [namespaces](./kibana-plugin-core-server.savedobjectscreateoptions.namespaces.md) | string[] | Optional initial namespaces for the object to be created in. If this is defined, it will supersede the namespace ID that is in [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md).Note: this can only be used for multi-namespace object types. | | [originId](./kibana-plugin-core-server.savedobjectscreateoptions.originid.md) | string | Optional ID of the original saved object, if this object's id was regenerated | | [overwrite](./kibana-plugin-core-server.savedobjectscreateoptions.overwrite.md) | boolean | Overwrite existing documents (defaults to false) | | [references](./kibana-plugin-core-server.savedobjectscreateoptions.references.md) | SavedObjectReference[] | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 4422b755faa77..0f362f302104b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -8,7 +8,7 @@ ```typescript esFilters: { - FilterLabel: typeof FilterLabel; + FilterLabel: (props: import("./ui/filter_bar/filter_editor/lib/filter_label").FilterLabelProps) => JSX.Element; FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filterbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filterbar.md deleted file mode 100644 index 6d8862323792a..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.filterbar.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) - -## FilterBar variable - -Signature: - -```typescript -FilterBar: React.ComponentClass, any> & { - WrappedComponent: React.ComponentType; -} -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect._constructor_.md deleted file mode 100644 index 4c08e8c862613..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect._constructor_.md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [(constructor)](./kibana-plugin-plugins-data-public.indexpatternselect._constructor_.md) - -## IndexPatternSelect.(constructor) - -Constructs a new instance of the `IndexPatternSelect` class - -Signature: - -```typescript -constructor(props: IndexPatternSelectProps); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| props | IndexPatternSelectProps | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentdidmount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentdidmount.md deleted file mode 100644 index cf70c2add8742..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentdidmount.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [componentDidMount](./kibana-plugin-plugins-data-public.indexpatternselect.componentdidmount.md) - -## IndexPatternSelect.componentDidMount() method - -Signature: - -```typescript -componentDidMount(): void; -``` -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentwillunmount.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentwillunmount.md deleted file mode 100644 index 5f11208ecc317..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.componentwillunmount.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [componentWillUnmount](./kibana-plugin-plugins-data-public.indexpatternselect.componentwillunmount.md) - -## IndexPatternSelect.componentWillUnmount() method - -Signature: - -```typescript -componentWillUnmount(): void; -``` -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.debouncedfetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.debouncedfetch.md deleted file mode 100644 index 5238e2f1913e4..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.debouncedfetch.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [debouncedFetch](./kibana-plugin-plugins-data-public.indexpatternselect.debouncedfetch.md) - -## IndexPatternSelect.debouncedFetch property - -Signature: - -```typescript -debouncedFetch: ((searchValue: string) => Promise) & _.Cancelable; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchoptions.md deleted file mode 100644 index f5e388a86f4ae..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchoptions.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [fetchOptions](./kibana-plugin-plugins-data-public.indexpatternselect.fetchoptions.md) - -## IndexPatternSelect.fetchOptions property - -Signature: - -```typescript -fetchOptions: (searchValue?: string) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchselectedindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchselectedindexpattern.md deleted file mode 100644 index d5981c19b99af..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.fetchselectedindexpattern.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [fetchSelectedIndexPattern](./kibana-plugin-plugins-data-public.indexpatternselect.fetchselectedindexpattern.md) - -## IndexPatternSelect.fetchSelectedIndexPattern property - -Signature: - -```typescript -fetchSelectedIndexPattern: (indexPatternId: string) => Promise; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.md deleted file mode 100644 index 4f4feeb4caa8d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.md +++ /dev/null @@ -1,37 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) - -## IndexPatternSelect class - -Signature: - -```typescript -export declare class IndexPatternSelect extends Component -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(props)](./kibana-plugin-plugins-data-public.indexpatternselect._constructor_.md) | | Constructs a new instance of the IndexPatternSelect class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [debouncedFetch](./kibana-plugin-plugins-data-public.indexpatternselect.debouncedfetch.md) | | ((searchValue: string) => Promise<void>) & _.Cancelable | | -| [fetchOptions](./kibana-plugin-plugins-data-public.indexpatternselect.fetchoptions.md) | | (searchValue?: string) => void | | -| [fetchSelectedIndexPattern](./kibana-plugin-plugins-data-public.indexpatternselect.fetchselectedindexpattern.md) | | (indexPatternId: string) => Promise<void> | | -| [onChange](./kibana-plugin-plugins-data-public.indexpatternselect.onchange.md) | | (selectedOptions: any) => void | | -| [state](./kibana-plugin-plugins-data-public.indexpatternselect.state.md) | | IndexPatternSelectState | | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [componentDidMount()](./kibana-plugin-plugins-data-public.indexpatternselect.componentdidmount.md) | | | -| [componentWillUnmount()](./kibana-plugin-plugins-data-public.indexpatternselect.componentwillunmount.md) | | | -| [render()](./kibana-plugin-plugins-data-public.indexpatternselect.render.md) | | | -| [UNSAFE\_componentWillReceiveProps(nextProps)](./kibana-plugin-plugins-data-public.indexpatternselect.unsafe_componentwillreceiveprops.md) | | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.onchange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.onchange.md deleted file mode 100644 index c0c2a2e6802e9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.onchange.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [onChange](./kibana-plugin-plugins-data-public.indexpatternselect.onchange.md) - -## IndexPatternSelect.onChange property - -Signature: - -```typescript -onChange: (selectedOptions: any) => void; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.render.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.render.md deleted file mode 100644 index 1cb495e7f8795..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.render.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [render](./kibana-plugin-plugins-data-public.indexpatternselect.render.md) - -## IndexPatternSelect.render() method - -Signature: - -```typescript -render(): JSX.Element; -``` -Returns: - -`JSX.Element` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.state.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.state.md deleted file mode 100644 index 58fbcfe090235..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.state.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [state](./kibana-plugin-plugins-data-public.indexpatternselect.state.md) - -## IndexPatternSelect.state property - -Signature: - -```typescript -state: IndexPatternSelectState; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.unsafe_componentwillreceiveprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.unsafe_componentwillreceiveprops.md deleted file mode 100644 index de9d6a69e216e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselect.unsafe_componentwillreceiveprops.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) > [UNSAFE\_componentWillReceiveProps](./kibana-plugin-plugins-data-public.indexpatternselect.unsafe_componentwillreceiveprops.md) - -## IndexPatternSelect.UNSAFE\_componentWillReceiveProps() method - -Signature: - -```typescript -UNSAFE_componentWillReceiveProps(nextProps: IndexPatternSelectProps): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| nextProps | IndexPatternSelectProps | | - -Returns: - -`void` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md new file mode 100644 index 0000000000000..1fe551def29ba --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternselectprops.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSelectProps](./kibana-plugin-plugins-data-public.indexpatternselectprops.md) + +## IndexPatternSelectProps type + +Signature: + +```typescript +export declare type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { + indexPatternId: string; + fieldTypes?: string[]; + onNoIndexPatterns?: () => void; + savedObjectsClient: SavedObjectsClientContract; +}; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index e5f56a1ec387f..f8897a059377d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -16,7 +16,6 @@ | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | | [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | -| [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | | [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | | [PainlessError](./kibana-plugin-plugins-data-public.painlesserror.md) | | @@ -113,7 +112,6 @@ | [extractSearchSourceReferences](./kibana-plugin-plugins-data-public.extractsearchsourcereferences.md) | | | [fieldFormats](./kibana-plugin-plugins-data-public.fieldformats.md) | | | [fieldList](./kibana-plugin-plugins-data-public.fieldlist.md) | | -| [FilterBar](./kibana-plugin-plugins-data-public.filterbar.md) | | | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | @@ -160,6 +158,7 @@ | [IMetricAggType](./kibana-plugin-plugins-data-public.imetricaggtype.md) | | | [IndexPatternAggRestrictions](./kibana-plugin-plugins-data-public.indexpatternaggrestrictions.md) | | | [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | | +| [IndexPatternSelectProps](./kibana-plugin-plugins-data-public.indexpatternselectprops.md) | | | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index aa7c3bb5d4932..3a0786a110ab6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC +QueryStringInput: (props: QueryStringInputProps) => JSX.Element ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md new file mode 100644 index 0000000000000..b00618f510510 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) + +## ACTION\_VISUALIZE\_LENS\_FIELD variable + +Signature: + +```typescript +ACTION_VISUALIZE_LENS_FIELD = "ACTION_VISUALIZE_LENS_FIELD" +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md new file mode 100644 index 0000000000000..96370a07806d3 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) > [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md) + +## ActionContextMapping.ACTION\_VISUALIZE\_LENS\_FIELD property + +Signature: + +```typescript +[ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md index 740e6ac63bfba..f83632dea0aa9 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md @@ -17,4 +17,5 @@ export interface ActionContextMapping | [""](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.__.md) | BaseContext | | | [ACTION\_VISUALIZE\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_field.md) | VisualizeFieldContext | | | [ACTION\_VISUALIZE\_GEO\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_geo_field.md) | VisualizeFieldContext | | +| [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.action_visualize_lens_field.md) | VisualizeFieldContext | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md index ce4e8c17b9dff..5e10de4e0f2a5 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md @@ -39,6 +39,7 @@ | --- | --- | | [ACTION\_VISUALIZE\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_field.md) | | | [ACTION\_VISUALIZE\_GEO\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_geo_field.md) | | +| [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) | | | [APPLY\_FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.apply_filter_trigger.md) | | | [applyFilterTrigger](./kibana-plugin-plugins-ui_actions-public.applyfiltertrigger.md) | | | [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.select_range_trigger.md) | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index 1782eef92442c..ba9060e01e57d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md index 0c4584a07b569..3e433809f9471 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; +readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index c65a9a992da2e..83afcab29689d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: T) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index 751abe332b08e..879f5a3d8628a 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index c372eb113d682..7fade7c4c841b 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,19 +21,19 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | | [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | | [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | -| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | +| [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | | [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[]> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | -| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | +| [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK"> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | | [triggers](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggers.md) | | TriggerRegistry | | | [triggerToActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.triggertoactions.md) | | TriggerToActionsRegistry | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md index c71e86fc09dc7..eeda7b503037d 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; +readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; ``` diff --git a/docs/ingest_manager/ingest-manager.asciidoc b/docs/fleet/fleet.asciidoc similarity index 67% rename from docs/ingest_manager/ingest-manager.asciidoc rename to docs/fleet/fleet.asciidoc index 8f6e8036c68cd..7039468f4b185 100644 --- a/docs/ingest_manager/ingest-manager.asciidoc +++ b/docs/fleet/fleet.asciidoc @@ -1,11 +1,11 @@ [chapter] [role="xpack"] -[[ingest-manager]] -= {ingest-manager} +[[fleet]] += {fleet} experimental[] -{ingest-manager} in {kib} enables you to add and manage integrations for popular +{fleet} in {kib} enables you to add and manage integrations for popular services and platforms, as well as manage {elastic-agent} installations in standalone or {fleet} mode. @@ -17,11 +17,13 @@ Standalone mode requires you to manually configure and manage the agent locally. * An overview of the data ingest in your {es} cluster. * Multiple integrations to collect and transform data. +//TODO: Redo screen capture. + [role="screenshot"] -image::ingest_manager/images/ingest-manager-start.png[{ingest-manager} app in {kib}] +image::fleet/images/fleet-start.png[{fleet} app in {kib}] [float] == Get started -To get started with {ingest-management}, refer to the +To get started with {fleet}, refer to the {ingest-guide}/index.html[Ingest Management Guide]. diff --git a/docs/fleet/images/fleet-start.png b/docs/fleet/images/fleet-start.png new file mode 100644 index 0000000000000..60e5416fde127 Binary files /dev/null and b/docs/fleet/images/fleet-start.png differ diff --git a/docs/ingest_manager/images/ingest-manager-start.png b/docs/ingest_manager/images/ingest-manager-start.png deleted file mode 100644 index 89174686a9768..0000000000000 Binary files a/docs/ingest_manager/images/ingest-manager-start.png and /dev/null differ diff --git a/docs/settings/fleet-settings.asciidoc b/docs/settings/fleet-settings.asciidoc new file mode 100644 index 0000000000000..9c28d28003175 --- /dev/null +++ b/docs/settings/fleet-settings.asciidoc @@ -0,0 +1,51 @@ +[role="xpack"] +[[fleet-settings-kb]] +=== {fleet} settings in {kib} +++++ +{fleet} settings +++++ + +experimental[] + +You can configure `xpack.fleet` settings in your `kibana.yml`. +By default, {fleet} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. + +See the {ingest-guide}/index.html[Ingest Management] docs for more information. + +[[general-fleet-settings-kb]] +==== General {fleet} settings + +[cols="2*<"] +|=== +| `xpack.fleet.enabled` {ess-icon} + | Set to `true` (default) to enable {fleet}. +| `xpack.fleet.agents.enabled` {ess-icon} + | Set to `true` (default) to enable {fleet}. +|=== + +[[fleet-data-visualizer-settings]] + +==== {package-manager} settings + +[cols="2*<"] +|=== +| `xpack.fleet.registryUrl` + | The address to use to reach {package-manager} registry. +|=== + +==== {fleet} settings + +[cols="2*<"] +|=== +| `xpack.fleet.agents.kibana.host` + | The hostname used by {agent} for accessing {kib}. +| `xpack.fleet.agents.elasticsearch.host` + | The hostname used by {agent} for accessing {es}. +| `xpack.fleet.agents.tlsCheckDisabled` + | Set to `true` to allow {fleet} to run on a {kib} instance without TLS enabled. +|=== + +[NOTE] +==== +In {ecloud}, {fleet} flags are already configured. +==== diff --git a/docs/settings/ingest-manager-settings.asciidoc b/docs/settings/ingest-manager-settings.asciidoc deleted file mode 100644 index 30e11f726c26b..0000000000000 --- a/docs/settings/ingest-manager-settings.asciidoc +++ /dev/null @@ -1,51 +0,0 @@ -[role="xpack"] -[[ingest-manager-settings-kb]] -=== {ingest-manager} settings in {kib} -++++ -{ingest-manager} settings -++++ - -experimental[] - -You can configure `xpack.ingestManager` settings in your `kibana.yml`. -By default, {ingest-manager} is enabled. To use {fleet}, you also need to configure {kib} and {es} hosts. - -See the {ingest-guide}/index.html[Ingest Management] docs for more information. - -[[general-ingest-manager-settings-kb]] -==== General {ingest-manager} settings - -[cols="2*<"] -|=== -| `xpack.ingestManager.enabled` {ess-icon} - | Set to `true` (default) to enable {ingest-manager}. -| `xpack.ingestManager.fleet.enabled` {ess-icon} - | Set to `true` (default) to enable {fleet}. -|=== - -[[ingest-manager-data-visualizer-settings]] - -==== {package-manager} settings - -[cols="2*<"] -|=== -| `xpack.ingestManager.registryUrl` - | The address to use to reach {package-manager} registry. -|=== - -==== {fleet} settings - -[cols="2*<"] -|=== -| `xpack.ingestManager.fleet.kibana.host` - | The hostname used by {agent} for accessing {kib}. -| `xpack.ingestManager.fleet.elasticsearch.host` - | The hostname used by {agent} for accessing {es}. -| `xpack.ingestManager.fleet.tlsCheckDisabled` - | Set to `true` to allow {fleet} to run on a {kib} instance without TLS enabled. -|=== - -[NOTE] -==== -In {ecloud}, {fleet} flags are already configured. -==== diff --git a/docs/settings/settings-xkb.asciidoc b/docs/settings/settings-xkb.asciidoc index 04fed0d6204b7..9d9cc92401896 100644 --- a/docs/settings/settings-xkb.asciidoc +++ b/docs/settings/settings-xkb.asciidoc @@ -20,4 +20,4 @@ include::ml-settings.asciidoc[] include::reporting-settings.asciidoc[] include::spaces-settings.asciidoc[] include::i18n-settings.asciidoc[] -include::ingest-manager-settings.asciidoc[] +include::fleet-settings.asciidoc[] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 903bb59cef380..2f2c87ca9c7d4 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -638,7 +638,7 @@ include::{kib-repo-dir}/settings/alert-action-settings.asciidoc[] include::{kib-repo-dir}/settings/apm-settings.asciidoc[] include::{kib-repo-dir}/settings/dev-settings.asciidoc[] include::{kib-repo-dir}/settings/graph-settings.asciidoc[] -include::{kib-repo-dir}/settings/ingest-manager-settings.asciidoc[] +include::{kib-repo-dir}/settings/fleet-settings.asciidoc[] include::{kib-repo-dir}/settings/i18n-settings.asciidoc[] include::{kib-repo-dir}/settings/logs-ui-settings.asciidoc[] include::{kib-repo-dir}/settings/infrastructure-ui-settings.asciidoc[] diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index c8bff91be91a6..4fa4f9860c2bd 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -209,18 +209,18 @@ visualization, configure the customization options for your visualization. The data fields that are displayed are based on the selected <> and the <>. -To view the data fields in a different index pattern, click the index pattern, then select a new one. The data fields automatically update. +To view the data fields in a different index pattern, click the *Change Index Pattern* dropdown, then select a new one. To filter the data fields: -* Enter the name in the *Search field names*. -* Click *Field by type*, then select the filter. To show all fields in the index pattern, deselect *Only show fields with data*. +* Enter the data field name in *Search field names*. +* Click *Field filters*, then select the filter. [float] [[view-data-summaries]] -==== View data summaries +==== View data field summaries -To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of +To help you decide exactly the data you want to display, get a quick summary of each data field. The summary shows the distribution of values within the specified time range. To view the data field summary information, navigate to the field, then click *i*. @@ -252,11 +252,9 @@ When there is an exclamation point (!) next to a visualization type, *Lens* is u For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type. -. Click a data field name in the editor, or click *Drop a field here*. -. Change the options that appear. -+ -[role="screenshot"] -image::images/lens_aggregation_labels.png[Quick function options] +. From the editor, click a data field, or click *Drop a field or click to add*. + +. Change the options, then click *Close*. [float] [[add-layers-and-indices]] diff --git a/docs/user/dashboard/images/lens_aggregation_labels.png b/docs/user/dashboard/images/lens_aggregation_labels.png deleted file mode 100644 index 9dcf1d226a197..0000000000000 Binary files a/docs/user/dashboard/images/lens_aggregation_labels.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_drag_drop.gif b/docs/user/dashboard/images/lens_drag_drop.gif index ca62115e7ea3a..22939467daa12 100644 Binary files a/docs/user/dashboard/images/lens_drag_drop.gif and b/docs/user/dashboard/images/lens_drag_drop.gif differ diff --git a/docs/user/dashboard/images/lens_index_pattern.png b/docs/user/dashboard/images/lens_index_pattern.png index 90a34b7a5d225..0c89e7ab7f814 100644 Binary files a/docs/user/dashboard/images/lens_index_pattern.png and b/docs/user/dashboard/images/lens_index_pattern.png differ diff --git a/docs/user/dashboard/images/lens_layers.png b/docs/user/dashboard/images/lens_layers.png index 7410425a6977e..5bc4217b7fb7d 100644 Binary files a/docs/user/dashboard/images/lens_layers.png and b/docs/user/dashboard/images/lens_layers.png differ diff --git a/docs/user/dashboard/images/lens_suggestions.gif b/docs/user/dashboard/images/lens_suggestions.gif index 3258e924cb205..5ba6aa5dee14d 100644 Binary files a/docs/user/dashboard/images/lens_suggestions.gif and b/docs/user/dashboard/images/lens_suggestions.gif differ diff --git a/docs/user/dashboard/images/lens_tutorial_1.png b/docs/user/dashboard/images/lens_tutorial_1.png new file mode 100644 index 0000000000000..047701fa495a7 Binary files /dev/null and b/docs/user/dashboard/images/lens_tutorial_1.png differ diff --git a/docs/user/dashboard/images/lens_tutorial_2.png b/docs/user/dashboard/images/lens_tutorial_2.png new file mode 100644 index 0000000000000..c3e5992778985 Binary files /dev/null and b/docs/user/dashboard/images/lens_tutorial_2.png differ diff --git a/docs/user/dashboard/images/lens_tutorial_3.1.png b/docs/user/dashboard/images/lens_tutorial_3.1.png new file mode 100644 index 0000000000000..23d9491c315e4 Binary files /dev/null and b/docs/user/dashboard/images/lens_tutorial_3.1.png differ diff --git a/docs/user/dashboard/images/lens_tutorial_3.2.png b/docs/user/dashboard/images/lens_tutorial_3.2.png new file mode 100644 index 0000000000000..cfe10fa1acfcf Binary files /dev/null and b/docs/user/dashboard/images/lens_tutorial_3.2.png differ diff --git a/docs/user/dashboard/images/lens_tutorial_3.png b/docs/user/dashboard/images/lens_tutorial_3.png new file mode 100644 index 0000000000000..891f24334d720 Binary files /dev/null and b/docs/user/dashboard/images/lens_tutorial_3.png differ diff --git a/docs/user/dashboard/images/lens_viz_types.png b/docs/user/dashboard/images/lens_viz_types.png index 2ecfa6bd0e0e3..0060234667f4e 100644 Binary files a/docs/user/dashboard/images/lens_viz_types.png and b/docs/user/dashboard/images/lens_viz_types.png differ diff --git a/docs/user/dashboard/tutorials.asciidoc b/docs/user/dashboard/tutorials.asciidoc index 931720ccbe257..b04de5fd0da6f 100644 --- a/docs/user/dashboard/tutorials.asciidoc +++ b/docs/user/dashboard/tutorials.asciidoc @@ -6,13 +6,13 @@ Learn how to use *Lens*, *Vega*, and *Timelion* by going through one of the step [[lens-tutorial]] === Compare sales over time with Lens -Ready to create your own visualization with Lens? Use the following tutorial to create a visualization that lets you compare sales over time. +Ready to create your own visualization with *Lens*? Use the following tutorial to create a visualization that lets you compare sales over time. [float] [[lens-before-begin]] ==== Before you begin -To start, you'll need to add the <>. +To start, add the <>. [float] ==== Build the visualization @@ -23,16 +23,16 @@ Drag and drop your data onto the visualization builder pane. . Click image:images/time-filter-calendar.png[], then click *Last 7 days*. + -The fields in the data panel update. +The *Available fields* automatically update. . Drag and drop the *taxful_total_price* data field to the visualization builder pane. + [role="screenshot"] image::images/lens_tutorial_1.png[Lens tutorial] -To display the average order prices over time, *Lens* automatically added in *order_date* field. +To display the average order prices over time, *Lens* automatically added *order_date* to the *X-axis*. -To break down your data, drag the *category.keyword* field to the visualization builder pane. Lens +To break down your data, drag and drop the *category.keyword* field to the visualization builder pane. Lens knows that you want to show the top categories and compare them across the dates, and creates a chart that compares the sales for each of the top three categories: @@ -45,30 +45,33 @@ image::images/lens_tutorial_2.png[Lens tutorial] Make your visualization look exactly how you want with the customization options. -. Click *Average of taxful_total_price*, then change the *Label* to `Sales`. +. Click *Average of taxful_total_price*, then change the *Display name* to Sales. + [role="screenshot"] image::images/lens_tutorial_3.1.png[Lens tutorial] +. Click *Close*. + . Click *Top values of category.keyword*, then change *Number of values* to `10`. + [role="screenshot"] image::images/lens_tutorial_3.2.png[Lens tutorial] + +. Click *Close*. + The visualization updates to show there are only six available categories. -+ -Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. -. To switch the chart type, click *Stacked bar chart* in the column, then click *Stacked area* from the *Select a visualizations* window. +. Look at the *Suggestions*. An area chart is not an option, but for the sales data, a stacked area chart might be the best option. +To switch the chart type, click *Stacked bar chart*, then click *Stacked area* from the *Select a visualization* dropdown. + [role="screenshot"] image::images/lens_tutorial_3.png[Lens tutorial] [float] [[lens-tutorial-next-steps]] -==== Next steps +==== What's next? -Now that you've created your visualization, you can add it to a <> or <>. +Now that you've created your *Lens* visualization, add it to a <> or <>. [[vega-lite-tutorial-create-your-first-visualizations]] === Create your first visualization with Vega-Lite diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 0bc77ab0a417e..6fd30690b988e 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -14,7 +14,7 @@ For additional *Vega* and *Vega-Lite* information, refer to the reference sectio * Automatic sizing * Default theme to match {kib} * Writing {es} queries using the time range and filters from dashboards -* Using the Elastic Map Service in Vega maps +* experimental[] Using the Elastic Map Service in Vega maps * Additional tooltip styling * Advanced setting to enable URL loading from any domain * Limited debugging support using the browser dev tools diff --git a/docs/user/index.asciidoc b/docs/user/index.asciidoc index e909626c5779c..b0f3dbfa0c9e9 100644 --- a/docs/user/index.asciidoc +++ b/docs/user/index.asciidoc @@ -43,7 +43,7 @@ include::monitoring/index.asciidoc[] include::management.asciidoc[] -include::{kib-repo-dir}/ingest_manager/ingest-manager.asciidoc[] +include::{kib-repo-dir}/fleet/fleet.asciidoc[] include::reporting/index.asciidoc[] diff --git a/docs/visualize/images/lens_aggregation_labels.png b/docs/visualize/images/lens_aggregation_labels.png deleted file mode 100644 index 9dcf1d226a197..0000000000000 Binary files a/docs/visualize/images/lens_aggregation_labels.png and /dev/null differ diff --git a/docs/visualize/images/lens_data_info.png b/docs/visualize/images/lens_data_info.png deleted file mode 100644 index 5ea6fc64a217d..0000000000000 Binary files a/docs/visualize/images/lens_data_info.png and /dev/null differ diff --git a/docs/visualize/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif deleted file mode 100644 index 1f8580d462702..0000000000000 Binary files a/docs/visualize/images/lens_drag_drop.gif and /dev/null differ diff --git a/docs/visualize/images/lens_index_pattern.png b/docs/visualize/images/lens_index_pattern.png deleted file mode 100644 index 90a34b7a5d225..0000000000000 Binary files a/docs/visualize/images/lens_index_pattern.png and /dev/null differ diff --git a/docs/visualize/images/lens_layers.png b/docs/visualize/images/lens_layers.png deleted file mode 100644 index 7410425a6977e..0000000000000 Binary files a/docs/visualize/images/lens_layers.png and /dev/null differ diff --git a/docs/visualize/images/lens_suggestions.gif b/docs/visualize/images/lens_suggestions.gif deleted file mode 100644 index 3258e924cb205..0000000000000 Binary files a/docs/visualize/images/lens_suggestions.gif and /dev/null differ diff --git a/docs/visualize/images/lens_tutorial_1.png b/docs/visualize/images/lens_tutorial_1.png deleted file mode 100644 index 77c1532e0ddac..0000000000000 Binary files a/docs/visualize/images/lens_tutorial_1.png and /dev/null differ diff --git a/docs/visualize/images/lens_tutorial_2.png b/docs/visualize/images/lens_tutorial_2.png deleted file mode 100644 index e7b8a7b515f52..0000000000000 Binary files a/docs/visualize/images/lens_tutorial_2.png and /dev/null differ diff --git a/docs/visualize/images/lens_tutorial_3.1.png b/docs/visualize/images/lens_tutorial_3.1.png deleted file mode 100644 index e9ed365e64aec..0000000000000 Binary files a/docs/visualize/images/lens_tutorial_3.1.png and /dev/null differ diff --git a/docs/visualize/images/lens_tutorial_3.2.png b/docs/visualize/images/lens_tutorial_3.2.png deleted file mode 100644 index c19bcb05dcb00..0000000000000 Binary files a/docs/visualize/images/lens_tutorial_3.2.png and /dev/null differ diff --git a/docs/visualize/images/lens_tutorial_3.png b/docs/visualize/images/lens_tutorial_3.png deleted file mode 100644 index 35fb10d4985e1..0000000000000 Binary files a/docs/visualize/images/lens_tutorial_3.png and /dev/null differ diff --git a/docs/visualize/images/lens_viz_types.png b/docs/visualize/images/lens_viz_types.png deleted file mode 100644 index 2ecfa6bd0e0e3..0000000000000 Binary files a/docs/visualize/images/lens_viz_types.png and /dev/null differ diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json index 0ac40ae1889de..223b8c55a5fde 100644 --- a/examples/embeddable_examples/kibana.json +++ b/examples/embeddable_examples/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["embeddable", "uiActions", "dashboard"], + "requiredPlugins": ["embeddable", "uiActions", "dashboard", "savedObjects"], "optionalPlugins": [], "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"], "requiredBundles": ["kibanaReact"] diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx index 292261ee16c59..a535552282150 100644 --- a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx +++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx @@ -33,12 +33,19 @@ import { BookEmbeddableOutput, } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; -import { OverlayStart } from '../../../../src/core/public'; +import { + OverlayStart, + SavedObjectsClientContract, + SimpleSavedObject, +} from '../../../../src/core/public'; import { DashboardStart, AttributeService } from '../../../../src/plugins/dashboard/public'; +import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; interface StartServices { getAttributeService: DashboardStart['getAttributeService']; openModal: OverlayStart['openModal']; + savedObjectsClient: SavedObjectsClientContract; + overlays: OverlayStart; } export type BookEmbeddableFactory = EmbeddableFactory< @@ -117,11 +124,55 @@ export class BookEmbeddableFactoryDefinition }); } + private async unwrapMethod(savedObjectId: string): Promise { + const { savedObjectsClient } = await this.getStartServices(); + const savedObject: SimpleSavedObject = await savedObjectsClient.get< + BookSavedObjectAttributes + >(this.type, savedObjectId); + return { ...savedObject.attributes }; + } + + private async saveMethod( + type: string, + attributes: BookSavedObjectAttributes, + savedObjectId?: string + ) { + const { savedObjectsClient } = await this.getStartServices(); + if (savedObjectId) { + return savedObjectsClient.update(type, savedObjectId, attributes); + } + return savedObjectsClient.create(type, attributes); + } + + private async checkForDuplicateTitleMethod(props: OnSaveProps): Promise { + const start = await this.getStartServices(); + const { savedObjectsClient, overlays } = start; + return checkForDuplicateTitle( + { + title: props.newTitle, + copyOnSave: false, + lastSavedTitle: '', + getEsType: () => this.type, + getDisplayName: this.getDisplayName || (() => this.type), + }, + props.isTitleDuplicateConfirmed, + props.onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } + private async getAttributeService() { if (!this.attributeService) { - this.attributeService = await (await this.getStartServices()).getAttributeService< + this.attributeService = (await this.getStartServices()).getAttributeService< BookSavedObjectAttributes - >(this.type); + >(this.type, { + saveMethod: this.saveMethod.bind(this), + unwrapMethod: this.unwrapMethod.bind(this), + checkForDuplicateTitle: this.checkForDuplicateTitleMethod.bind(this), + }); } return this.attributeService!; } diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx index 3541ace1e5e7e..77035b6887734 100644 --- a/examples/embeddable_examples/public/book/edit_book_action.tsx +++ b/examples/embeddable_examples/public/book/edit_book_action.tsx @@ -31,10 +31,13 @@ import { } from './book_embeddable'; import { CreateEditBookComponent } from './create_edit_book_component'; import { DashboardStart } from '../../../../src/plugins/dashboard/public'; +import { OnSaveProps } from '../../../../src/plugins/saved_objects/public'; +import { SavedObjectsClientContract } from '../../../../src/core/target/types/public/saved_objects'; interface StartServices { openModal: OverlayStart['openModal']; getAttributeService: DashboardStart['getAttributeService']; + savedObjectsClient: SavedObjectsClientContract; } interface ActionContext { @@ -56,8 +59,24 @@ export const createEditBookAction = (getStartServices: () => Promise { - const { openModal, getAttributeService } = await getStartServices(); - const attributeService = getAttributeService(BOOK_SAVED_OBJECT); + const { openModal, getAttributeService, savedObjectsClient } = await getStartServices(); + const attributeService = getAttributeService(BOOK_SAVED_OBJECT, { + saveMethod: async ( + type: string, + attributes: BookSavedObjectAttributes, + savedObjectId?: string + ) => { + if (savedObjectId) { + return savedObjectsClient.update(type, savedObjectId, attributes); + } + return savedObjectsClient.create(type, attributes); + }, + checkForDuplicateTitle: (props: OnSaveProps) => { + return new Promise(() => { + return true; + }); + }, + }); const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => { const newInput = await attributeService.wrapAttributes( attributes, diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 0c6ed1eb3be48..6d1b119e741bd 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -22,7 +22,7 @@ import { EmbeddableStart, CONTEXT_MENU_TRIGGER, } from '../../../src/plugins/embeddable/public'; -import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public'; +import { Plugin, CoreSetup, CoreStart, SavedObjectsClient } from '../../../src/core/public'; import { HelloWorldEmbeddableFactory, HELLO_WORLD_EMBEDDABLE, @@ -76,6 +76,7 @@ export interface EmbeddableExamplesSetupDependencies { export interface EmbeddableExamplesStartDependencies { embeddable: EmbeddableStart; dashboard: DashboardStart; + savedObjectsClient: SavedObjectsClient; } interface ExampleEmbeddableFactories { @@ -158,12 +159,15 @@ export class EmbeddableExamplesPlugin new BookEmbeddableFactoryDefinition(async () => ({ getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, + overlays: (await core.getStartServices())[0].overlays, })) ); const editBookAction = createEditBookAction(async () => ({ getAttributeService: (await core.getStartServices())[1].dashboard.getAttributeService, openModal: (await core.getStartServices())[0].overlays.openModal, + savedObjectsClient: (await core.getStartServices())[0].savedObjects.client, })); deps.uiActions.registerAction(editBookAction); deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id); diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index ab0ce185f0602..61e50d3379d03 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -53,7 +53,6 @@ import { import { DataPublicPluginStart, - IndexPatternSelect, IndexPattern, IndexPatternField, isCompleteResponse, @@ -92,6 +91,7 @@ export const SearchExamplesApp = ({ navigation, data, }: SearchExamplesAppDeps) => { + const { IndexPatternSelect } = data.ui; const [getCool, setGetCool] = useState(false); const [timeTook, setTimeTook] = useState(); const [indexPattern, setIndexPattern] = useState(); diff --git a/package.json b/package.json index b2252e2bd264b..9f9ad9ead7096 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "kbn:watch": "node scripts/kibana --dev --logging.json=false", "build:types": "rm -rf ./target/types && tsc --p tsconfig.types.json", "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", - "kbn:bootstrap": "node scripts/build_ts_refs --project tsconfig.refs.json && node scripts/register_git_hook", + "kbn:bootstrap": "node scripts/build_ts_refs && node scripts/register_git_hook", "spec_to_console": "node scripts/spec_to_console", "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", @@ -117,7 +117,7 @@ "dependencies": { "@elastic/datemath": "5.0.3", "@elastic/elasticsearch": "7.9.1", - "@elastic/eui": "29.0.0", + "@elastic/eui": "29.3.0", "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", @@ -230,7 +230,7 @@ "@babel/register": "^7.10.5", "@babel/types": "^7.11.0", "@elastic/apm-rum": "^5.6.1", - "@elastic/charts": "23.0.0", + "@elastic/charts": "23.2.1", "@elastic/ems-client": "7.10.0", "@elastic/eslint-config-kibana": "0.15.0", "@elastic/eslint-plugin-eui": "0.0.2", diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index fe6247673e312..83438215716ac 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -42,12 +42,13 @@ describe('ApmConfiguration', () => { resetAllMocks(); }); - it('sets the correct service name', () => { + it('sets the correct service name and version', () => { packageMock.raw = { version: '9.2.1', }; const config = new ApmConfiguration(mockedRootDir, {}, false); - expect(config.getConfig('myservice').serviceName).toBe('myservice-9_2_1'); + expect(config.getConfig('myservice').serviceName).toBe('myservice'); + expect(config.getConfig('myservice').serviceVersion).toBe('9.2.1'); }); it('sets the git revision from `git rev-parse` command in non distribution mode', () => { diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index aab82c6c06a58..897e7fd7ca610 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -30,8 +30,15 @@ const getDefaultConfig = (isDistributable: boolean): ApmAgentConfig => { return { active: false, globalLabels: {}, + // Do not use a centralized controlled config + centralConfig: false, + // Capture all exceptions that are not caught + logUncaughtExceptions: true, + // Can be performance intensive, disabling by default + breakdownMetrics: false, }; } + return { active: false, serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443', @@ -60,14 +67,14 @@ export class ApmConfiguration { ) { // eslint-disable-next-line @typescript-eslint/no-var-requires const { version, build } = require(join(this.rootDir, 'package.json')); - this.kibanaVersion = version.replace(/\./g, '_'); + this.kibanaVersion = version; this.pkgBuild = build; } public getConfig(serviceName: string): ApmAgentConfig { return { ...this.getBaseConfig(), - serviceName: `${serviceName}-${this.kibanaVersion}`, + serviceName, }; } @@ -76,7 +83,8 @@ export class ApmConfiguration { const apmConfig = merge( getDefaultConfig(this.isDistributable), this.getConfigFromKibanaConfig(), - this.getDevConfig() + this.getDevConfig(), + this.getDistConfig() ); const rev = this.getGitRev(); @@ -88,6 +96,8 @@ export class ApmConfiguration { if (uuid) { apmConfig.globalLabels.kibana_uuid = uuid; } + + apmConfig.serviceVersion = this.kibanaVersion; this.baseConfig = apmConfig; } @@ -123,6 +133,19 @@ export class ApmConfiguration { } } + /** Config keys that cannot be overridden in production builds */ + private getDistConfig(): ApmAgentConfig { + if (!this.isDistributable) { + return {}; + } + + return { + // Headers & body may contain sensitive info + captureHeaders: false, + captureBody: 'off', + }; + } + private getGitRev() { if (this.isDistributable) { return this.pkgBuild.sha; diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md index c7b98224c4e57..12fc33dfaffb0 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/README.md +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/README.md @@ -8,10 +8,23 @@ This class integrates with the `ciStats.trackBuild {}` Jenkins Pipeline function To create an instance of the reporter, import the class and call `CiStatsReporter.fromEnv(log)` (passing it a tooling log). -#### `CiStatsReporter#metrics(metrics: Array<{ group: string, id: string, value: number }>)` +#### `CiStatsReporter#metrics(metrics: Metric[])` Use this method to record metrics in the Kibana CI Stats service. +```ts +interface Metric { + group: string, + id: string, + value: number, + // optional limit, values which exceed the limit will fail PRs + limit?: number + // optional path, relative to the root of the repo, where config values + // are defined. Will be linked to in PRs which have overages. + limitConfigPath?: string +} +``` + Example: ```ts diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index b0378ab6c5cd5..a2f3b63daec50 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -29,7 +29,13 @@ interface Config { buildId: string; } -export type CiStatsMetrics = Array<{ group: string; id: string; value: number }>; +export type CiStatsMetrics = Array<{ + group: string; + id: string; + value: number; + limit?: number; + limitConfigPath?: string; +}>; function parseConfig(log: ToolingLog) { const configJson = process.env.KIBANA_CI_STATS_CONFIG; diff --git a/packages/kbn-i18n/src/react/index.tsx b/packages/kbn-i18n/src/react/index.tsx index ff30934aad6d1..b438c44598b75 100644 --- a/packages/kbn-i18n/src/react/index.tsx +++ b/packages/kbn-i18n/src/react/index.tsx @@ -17,8 +17,10 @@ * under the License. */ -import { InjectedIntl as _InjectedIntl } from 'react-intl'; +import { InjectedIntl as _InjectedIntl, InjectedIntlProps as _InjectedIntlProps } from 'react-intl'; + export type InjectedIntl = _InjectedIntl; +export type InjectedIntlProps = _InjectedIntlProps; export { intlShape, @@ -29,6 +31,8 @@ export { FormattedPlural, FormattedMessage, FormattedHTMLMessage, + // Only used for testing. Use I18nProvider otherwise. + IntlProvider as __IntlProvider, } from 'react-intl'; export { I18nProvider } from './provider'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml new file mode 100644 index 0000000000000..e395734928a4d --- /dev/null +++ b/packages/kbn-optimizer/limits.yml @@ -0,0 +1,100 @@ +pageLoadAssetSize: + advancedSettings: 27_596 + alerts: 106_936 + apm: 64_385 + apmOss: 18_996 + beatsManagement: 188_135 + bfetch: 41_874 + canvas: 1_066_647 + charts: 159_211 + cloud: 21_076 + console: 46_091 + core: 692_106 + crossClusterReplication: 65_408 + dashboard: 374_194 + dashboardEnhanced: 65_646 + dashboardMode: 22_716 + data: 1_170_713 + dataEnhanced: 50_420 + devTools: 38_637 + discover: 105_145 + discoverEnhanced: 42_730 + embeddable: 312_874 + embeddableEnhanced: 41_145 + enterpriseSearch: 35_741 + esUiShared: 326_654 + expressions: 224_136 + features: 31_211 + fileUpload: 24_717 + globalSearch: 43_548 + globalSearchBar: 62_888 + globalSearchProviders: 25_554 + graph: 31_504 + grokdebugger: 26_779 + home: 41_661 + indexLifecycleManagement: 107_090 + indexManagement: 140_608 + indexPatternManagement: 154_222 + infra: 197_873 + ingestManager: 415_829 + ingestPipelines: 58_003 + inputControlVis: 172_675 + inspector: 148_711 + kibanaLegacy: 107_711 + kibanaOverview: 56_279 + kibanaReact: 161_921 + kibanaUtils: 198_829 + lens: 96_624 + licenseManagement: 41_817 + licensing: 39_008 + lists: 183_665 + logstash: 53_548 + management: 46_112 + maps: 183_610 + mapsLegacy: 116_817 + mapsLegacyLicensing: 20_214 + ml: 82_187 + monitoring: 268_612 + navigation: 37_269 + newsfeed: 42_228 + observability: 89_709 + painlessLab: 179_748 + regionMap: 66_098 + remoteClusters: 51_327 + reporting: 183_418 + rollup: 97_204 + savedObjects: 108_518 + savedObjectsManagement: 100_503 + searchprofiler: 67_080 + security: 189_428 + securityOss: 30_806 + securitySolution: 622_387 + share: 99_061 + snapshotRestore: 79_032 + spaces: 387_915 + telemetry: 91_832 + telemetryManagementSection: 52_443 + tileMap: 65_337 + timelion: 29_920 + transform: 41_007 + triggersActionsUi: 170_001 + uiActions: 97_717 + uiActionsEnhanced: 349_511 + upgradeAssistant: 81_241 + uptime: 40_825 + urlDrilldown: 34_174 + urlForwarding: 32_579 + usageCollection: 39_762 + visDefaultEditor: 50_178 + visTypeMarkdown: 30_896 + visTypeMetric: 42_790 + visTypeTable: 94_934 + visTypeTagcloud: 37_575 + visTypeTimelion: 51_933 + visTypeTimeseries: 155_203 + visTypeVega: 153_573 + visTypeVislib: 242_838 + visTypeXy: 20_255 + visualizations: 295_025 + visualize: 57_431 + watcher: 43_598 diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index 52f9349aec696..5d9a409919db1 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -22,6 +22,7 @@ "cpy": "^8.0.0", "core-js": "^3.6.5", "css-loader": "^3.4.2", + "dedent": "^0.7.0", "del": "^5.1.0", "execa": "^4.0.2", "file-loader": "^4.2.0", @@ -38,6 +39,7 @@ "postcss-loader": "^3.0.0", "raw-loader": "^3.1.0", "rxjs": "^6.5.5", + "js-yaml": "^3.14.0", "sass-loader": "^8.0.2", "source-map-support": "^0.5.19", "style-loader": "^1.1.3", diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index dcfb56be66efd..386a7a5053734 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -28,6 +28,7 @@ import { logOptimizerState } from './log_optimizer_state'; import { OptimizerConfig } from './optimizer'; import { reportOptimizerStats } from './report_optimizer_stats'; import { runOptimizer } from './run_optimizer'; +import { validateLimitsForAllBundles, updateBundleLimits } from './limits'; run( async ({ log, flags }) => { @@ -93,14 +94,24 @@ run( throw createFlagError('expected --filter to be one or more strings'); } + const validateLimits = flags['validate-limits'] ?? false; + if (typeof validateLimits !== 'boolean') { + throw createFlagError('expected --validate-limits to have no value'); + } + + const updateLimits = flags['update-limits'] ?? false; + if (typeof updateLimits !== 'boolean') { + throw createFlagError('expected --update-limits to have no value'); + } + const config = OptimizerConfig.create({ repoRoot: REPO_ROOT, watch, maxWorkerCount, - oss, - dist, + oss: oss && !(validateLimits || updateLimits), + dist: dist || updateLimits, cache, - examples, + examples: examples && !(validateLimits || updateLimits), profileWebpack, extraPluginScanDirs, inspectWorkers, @@ -108,6 +119,11 @@ run( filter, }); + if (validateLimits) { + validateLimitsForAllBundles(log, config); + return; + } + let update$ = runOptimizer(config); if (reportStats) { @@ -121,6 +137,10 @@ run( } await update$.pipe(logOptimizerState(log, config)).toPromise(); + + if (updateLimits) { + updateBundleLimits(log, config); + } }, { flags: { @@ -134,6 +154,8 @@ run( 'profile', 'inspect-workers', 'report-stats', + 'validate-limits', + 'update-limits', ], string: ['workers', 'scan-dir', 'filter'], default: { @@ -152,10 +174,12 @@ run( --no-cache disable the cache --filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported --no-examples don't build the example plugins - --dist create bundles that are suitable for inclusion in the Kibana distributable + --dist create bundles that are suitable for inclusion in the Kibana distributable, enabled when running with --update-limits --scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary) --no-inspect-workers when inspecting the parent process, don't inspect the workers --report-stats attempt to report stats about this execution of the build to the kibana-ci-stats service using this name + --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle + --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb `, }, } diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 549e4b13a4ac0..c522ff770d369 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -22,3 +22,4 @@ export * from './run_optimizer'; export * from './log_optimizer_state'; export * from './report_optimizer_stats'; export * from './node'; +export * from './limits'; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 038beca703720..cb5bb1e8fc529 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,7 @@ OptimizerConfig { "cache": true, "dist": false, "inspectWorkers": false, + "limits": "", "maxWorkerCount": 1, "plugins": Array [ Object { diff --git a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts index de3838eb92975..3dff034af886c 100644 --- a/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts +++ b/packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts @@ -27,7 +27,13 @@ import del from 'del'; import { toArray, tap, filter } from 'rxjs/operators'; import { REPO_ROOT } from '@kbn/utils'; import { ToolingLog } from '@kbn/dev-utils'; -import { runOptimizer, OptimizerConfig, OptimizerUpdate, logOptimizerState } from '@kbn/optimizer'; +import { + runOptimizer, + OptimizerConfig, + OptimizerUpdate, + logOptimizerState, + readLimits, +} from '@kbn/optimizer'; const TMP_DIR = Path.resolve(__dirname, '../__fixtures__/__tmp__'); const MOCK_REPO_SRC = Path.resolve(__dirname, '../__fixtures__/mock_repo'); @@ -72,6 +78,9 @@ it('builds expected bundles, saves bundle counts to metadata', async () => { dist: false, }); + expect(config.limits).toEqual(readLimits()); + (config as any).limits = ''; + expect(config).toMatchSnapshot('OptimizerConfig'); const msgs = await runOptimizer(config) diff --git a/packages/kbn-optimizer/src/limits.ts b/packages/kbn-optimizer/src/limits.ts new file mode 100644 index 0000000000000..4040a0c37d3b6 --- /dev/null +++ b/packages/kbn-optimizer/src/limits.ts @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; + +import dedent from 'dedent'; +import Yaml from 'js-yaml'; +import { createFailError, ToolingLog } from '@kbn/dev-utils'; + +import { OptimizerConfig, getMetrics } from './optimizer'; + +const LIMITS_PATH = require.resolve('../limits.yml'); +const DEFAULT_BUDGET = 15000; + +const diff = (a: T[], b: T[]): T[] => a.filter((item) => !b.includes(item)); + +export function readLimits() { + return Yaml.safeLoad(Fs.readFileSync(LIMITS_PATH, 'utf8')); +} + +export function validateLimitsForAllBundles(log: ToolingLog, config: OptimizerConfig) { + const limitBundleIds = Object.keys(config.limits.pageLoadAssetSize); + const configBundleIds = config.bundles.map((b) => b.id); + + const missingBundleIds = diff(configBundleIds, limitBundleIds); + const extraBundleIds = diff(limitBundleIds, configBundleIds); + + const issues = []; + if (missingBundleIds.length) { + issues.push(`missing: ${missingBundleIds.join(', ')}`); + } + if (extraBundleIds.length) { + issues.push(`extra: ${extraBundleIds.join(', ')}`); + } + if (issues.length) { + throw createFailError( + dedent` + The limits defined in packages/kbn-optimizer/limits.yml are outdated. Please update + this file with a limit (in bytes) for every production bundle. + + ${issues.join('\n ')} + + To validate your changes locally, run: + + node scripts/build_kibana_platform_plugins.js --validate-limits + ` + '\n' + ); + } + + log.success('limits.yml file valid'); +} + +export function updateBundleLimits(log: ToolingLog, config: OptimizerConfig) { + const metrics = getMetrics(log, config); + + const number = (input: number) => input.toLocaleString('en').split(',').join('_'); + + let yaml = `pageLoadAssetSize:\n`; + for (const metric of metrics.sort((a, b) => a.id.localeCompare(b.id))) { + if (metric.group === 'page load bundle size') { + yaml += ` ${metric.id}: ${number(metric.value + DEFAULT_BUDGET)}\n`; + } + } + + Fs.writeFileSync(LIMITS_PATH, yaml); + log.success(`wrote updated limits to ${LIMITS_PATH}`); +} diff --git a/packages/kbn-optimizer/src/optimizer/get_output_stats.ts b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts new file mode 100644 index 0000000000000..24847a03edb52 --- /dev/null +++ b/packages/kbn-optimizer/src/optimizer/get_output_stats.ts @@ -0,0 +1,124 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Fs from 'fs'; +import Path from 'path'; + +import { ToolingLog, CiStatsMetrics } from '@kbn/dev-utils'; +import { OptimizerConfig } from './optimizer_config'; + +const flatten = (arr: Array): T[] => + arr.reduce((acc: T[], item) => acc.concat(item), []); + +interface Entry { + relPath: string; + stats: Fs.Stats; +} + +const IGNORED_EXTNAME = ['.map', '.br', '.gz']; + +const getFiles = (dir: string, parent?: string) => + flatten( + Fs.readdirSync(dir).map((name): Entry | Entry[] => { + const absPath = Path.join(dir, name); + const relPath = parent ? Path.join(parent, name) : name; + const stats = Fs.statSync(absPath); + + if (stats.isDirectory()) { + return getFiles(absPath, relPath); + } + + return { + relPath, + stats, + }; + }) + ).filter((file) => { + const filename = Path.basename(file.relPath); + if (filename.startsWith('.')) { + return false; + } + + const ext = Path.extname(filename); + if (IGNORED_EXTNAME.includes(ext)) { + return false; + } + + return true; + }); + +export function getMetrics(log: ToolingLog, config: OptimizerConfig) { + return flatten( + config.bundles.map((bundle) => { + // make the cache read from the cache file since it was likely updated by the worker + bundle.cache.refresh(); + + const outputFiles = getFiles(bundle.outputDir); + const entryName = `${bundle.id}.${bundle.type}.js`; + const entry = outputFiles.find((f) => f.relPath === entryName); + if (!entry) { + throw new Error( + `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` + ); + } + + const chunkPrefix = `${bundle.id}.chunk.`; + const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); + const miscFiles = outputFiles.filter((f) => f !== entry && !asyncChunks.includes(f)); + + if (asyncChunks.length) { + log.verbose(bundle.id, 'async chunks', asyncChunks); + } + if (miscFiles.length) { + log.verbose(bundle.id, 'misc files', asyncChunks); + } + + const sumSize = (files: Entry[]) => files.reduce((acc: number, f) => acc + f.stats!.size, 0); + + const bundleMetrics: CiStatsMetrics = [ + { + group: `@kbn/optimizer bundle module count`, + id: bundle.id, + value: bundle.cache.getModuleCount() || 0, + }, + { + group: `page load bundle size`, + id: bundle.id, + value: entry.stats!.size, + limit: config.limits.pageLoadAssetSize[bundle.id], + limitConfigPath: `packages/kbn-optimizer/limits.yml`, + }, + { + group: `async chunks size`, + id: bundle.id, + value: sumSize(asyncChunks), + }, + { + group: `miscellaneous assets size`, + id: bundle.id, + value: sumSize(miscFiles), + }, + ]; + + log.debug(bundle.id, 'metrics', bundleMetrics); + + return bundleMetrics; + }) + ); +} diff --git a/packages/kbn-optimizer/src/optimizer/index.ts b/packages/kbn-optimizer/src/optimizer/index.ts index 84fd395e98976..77df112b44351 100644 --- a/packages/kbn-optimizer/src/optimizer/index.ts +++ b/packages/kbn-optimizer/src/optimizer/index.ts @@ -25,3 +25,4 @@ export * from './watch_bundles_for_changes'; export * from './run_workers'; export * from './bundle_cache'; export * from './handle_optimizer_completion'; +export * from './get_output_stats'; diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index fd887e8c2c012..948ba520931e5 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -22,6 +22,7 @@ jest.mock('./kibana_platform_plugins.ts'); jest.mock('./get_plugin_bundles.ts'); jest.mock('../common/theme_tags.ts'); jest.mock('./filter_by_id.ts'); +jest.mock('../limits.ts'); jest.mock('os', () => { const realOs = jest.requireActual('os'); @@ -385,6 +386,7 @@ describe('OptimizerConfig::create()', () => { .findKibanaPlatformPlugins; const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles; const filterById: jest.Mock = jest.requireMock('./filter_by_id.ts').filterById; + const readLimits: jest.Mock = jest.requireMock('../limits.ts').readLimits; beforeEach(() => { if ('mock' in OptimizerConfig.parseOptions) { @@ -398,6 +400,7 @@ describe('OptimizerConfig::create()', () => { findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins')); getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]); filterById.mockReturnValue(Symbol('filtered bundles')); + readLimits.mockReturnValue(Symbol('limits')); jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): { [key in keyof ParsedOptions]: any; @@ -429,6 +432,7 @@ describe('OptimizerConfig::create()', () => { "cache": Symbol(parsed cache), "dist": Symbol(parsed dist), "inspectWorkers": Symbol(parsed inspect workers), + "limits": Symbol(limits), "maxWorkerCount": Symbol(parsed max worker count), "plugins": Symbol(new platform plugins), "profileWebpack": Symbol(parsed profile webpack), diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b1ab1ebfe49f2..01a20bec52a04 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -32,6 +32,13 @@ import { import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; import { filterById } from './filter_by_id'; +import { readLimits } from '../limits'; + +export interface Limits { + pageLoadAssetSize: { + [id: string]: number | undefined; + }; +} function pickMaxWorkerCount(dist: boolean) { // don't break if cpus() returns nothing, or an empty array @@ -238,7 +245,8 @@ export class OptimizerConfig { options.maxWorkerCount, options.dist, options.profileWebpack, - options.themeTags + options.themeTags, + readLimits() ); } @@ -252,7 +260,8 @@ export class OptimizerConfig { public readonly maxWorkerCount: number, public readonly dist: boolean, public readonly profileWebpack: boolean, - public readonly themeTags: ThemeTags + public readonly themeTags: ThemeTags, + public readonly limits: Limits ) {} getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig { diff --git a/packages/kbn-optimizer/src/report_optimizer_stats.ts b/packages/kbn-optimizer/src/report_optimizer_stats.ts index eff2bce0b827e..a0f59a3505e30 100644 --- a/packages/kbn-optimizer/src/report_optimizer_stats.ts +++ b/packages/kbn-optimizer/src/report_optimizer_stats.ts @@ -17,136 +17,41 @@ * under the License. */ -import Fs from 'fs'; -import Path from 'path'; - import { materialize, mergeMap, dematerialize } from 'rxjs/operators'; -import { CiStatsReporter, CiStatsMetrics, ToolingLog } from '@kbn/dev-utils'; +import { CiStatsReporter, ToolingLog } from '@kbn/dev-utils'; import { OptimizerUpdate$ } from './run_optimizer'; -import { OptimizerState, OptimizerConfig } from './optimizer'; +import { OptimizerConfig, getMetrics } from './optimizer'; import { pipeClosure } from './common'; -const flatten = (arr: Array): T[] => - arr.reduce((acc: T[], item) => acc.concat(item), []); - -interface Entry { - relPath: string; - stats: Fs.Stats; -} - -const IGNORED_EXTNAME = ['.map', '.br', '.gz']; - -const getFiles = (dir: string, parent?: string) => - flatten( - Fs.readdirSync(dir).map((name): Entry | Entry[] => { - const absPath = Path.join(dir, name); - const relPath = parent ? Path.join(parent, name) : name; - const stats = Fs.statSync(absPath); - - if (stats.isDirectory()) { - return getFiles(absPath, relPath); - } - - return { - relPath, - stats, - }; - }) - ).filter((file) => { - const filename = Path.basename(file.relPath); - if (filename.startsWith('.')) { - return false; - } - - const ext = Path.extname(filename); - if (IGNORED_EXTNAME.includes(ext)) { - return false; - } - - return true; - }); - export function reportOptimizerStats( reporter: CiStatsReporter, config: OptimizerConfig, log: ToolingLog ) { - return pipeClosure((update$: OptimizerUpdate$) => { - let lastState: OptimizerState | undefined; - return update$.pipe( + return pipeClosure((update$: OptimizerUpdate$) => + update$.pipe( materialize(), mergeMap(async (n) => { - if (n.kind === 'N' && n.value?.state) { - lastState = n.value?.state; - } - - if (n.kind === 'C' && lastState) { - await reporter.metrics( - flatten( - config.bundles.map((bundle) => { - // make the cache read from the cache file since it was likely updated by the worker - bundle.cache.refresh(); - - const outputFiles = getFiles(bundle.outputDir); - const entryName = `${bundle.id}.${bundle.type}.js`; - const entry = outputFiles.find((f) => f.relPath === entryName); - if (!entry) { - throw new Error( - `Unable to find bundle entry named [${entryName}] in [${bundle.outputDir}]` - ); - } - - const chunkPrefix = `${bundle.id}.chunk.`; - const asyncChunks = outputFiles.filter((f) => f.relPath.startsWith(chunkPrefix)); - const miscFiles = outputFiles.filter( - (f) => f !== entry && !asyncChunks.includes(f) - ); - - if (asyncChunks.length) { - log.verbose(bundle.id, 'async chunks', asyncChunks); - } - if (miscFiles.length) { - log.verbose(bundle.id, 'misc files', asyncChunks); - } - - const sumSize = (files: Entry[]) => - files.reduce((acc: number, f) => acc + f.stats!.size, 0); - - const metrics: CiStatsMetrics = [ - { - group: `@kbn/optimizer bundle module count`, - id: bundle.id, - value: bundle.cache.getModuleCount() || 0, - }, - { - group: `page load bundle size`, - id: bundle.id, - value: entry.stats!.size, - }, - { - group: `async chunks size`, - id: bundle.id, - value: sumSize(asyncChunks), - }, - { - group: `miscellaneous assets size`, - id: bundle.id, - value: sumSize(miscFiles), - }, - ]; - - log.info(bundle.id, 'metrics', metrics); - - return metrics; - }) - ) - ); + if (n.kind === 'C') { + const metrics = getMetrics(log, config); + + await reporter.metrics(metrics); + + for (const metric of metrics) { + if (metric.limit != null && metric.value > metric.limit) { + const value = metric.value.toLocaleString(); + const limit = metric.limit.toLocaleString(); + log.warning( + `Metric [${metric.group}] for [${metric.id}] of [${value}] over the limit of [${limit}]` + ); + } + } } return n; }), dematerialize() - ); - }); + ) + ); } diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 48dba22505232..2e50f4214beb4 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -196,6 +196,7 @@ function help() { --oss Do not include the x-pack when running command. --skip-kibana-plugins Filter all plugins in ./plugins and ../kibana-extra when running command. --no-cache Disable the bootstrap cache + --no-validate Disable the bootstrap yarn.lock validation --verbose Set log level to verbose --debug Set log level to debug --quiet Set log level to error @@ -222,9 +223,10 @@ async function run(argv) { i: 'include' }, default: { - cache: true + cache: true, + validate: true }, - boolean: ['prefer-offline', 'frozen-lockfile', 'cache'] + boolean: ['prefer-offline', 'frozen-lockfile', 'cache', 'validate'] }); const args = options._; @@ -8998,7 +9000,11 @@ const BootstrapCommand = { } const yarnLock = await Object(_utils_yarn_lock__WEBPACK_IMPORTED_MODULE_6__["readYarnLock"])(kbn); - await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + + if (options.validate) { + await Object(_utils_validate_yarn_lock__WEBPACK_IMPORTED_MODULE_7__["validateYarnLock"])(kbn, yarnLock); + } + await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_0__["linkProjectExecutables"])(projects, projectGraph); /** * At the end of the bootstrapping process we call all `kbn:bootstrap` scripts diff --git a/packages/kbn-pm/src/cli.ts b/packages/kbn-pm/src/cli.ts index 816e84c13bbe9..92ddf3d957cd5 100644 --- a/packages/kbn-pm/src/cli.ts +++ b/packages/kbn-pm/src/cli.ts @@ -47,6 +47,7 @@ function help() { --oss Do not include the x-pack when running command. --skip-kibana-plugins Filter all plugins in ./plugins and ../kibana-extra when running command. --no-cache Disable the bootstrap cache + --no-validate Disable the bootstrap yarn.lock validation --verbose Set log level to verbose --debug Set log level to debug --quiet Set log level to error @@ -80,8 +81,9 @@ export async function run(argv: string[]) { }, default: { cache: true, + validate: true, }, - boolean: ['prefer-offline', 'frozen-lockfile', 'cache'], + boolean: ['prefer-offline', 'frozen-lockfile', 'cache', 'validate'], }); const args = options._; diff --git a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap b/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap deleted file mode 100644 index be146d710c87a..0000000000000 --- a/packages/kbn-pm/src/commands/__snapshots__/bootstrap.test.ts.snap +++ /dev/null @@ -1,159 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: link bins 1`] = ` -Array [ - Array [ - Map { - "kibana" => Project { - "allDependencies": Object { - "bar": "1.0.0", - }, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": true, - "json": Object { - "dependencies": Object { - "bar": "1.0.0", - }, - "name": "kibana", - "version": "1.0.0", - "workspaces": Object { - "packages": Array [ - "packages/*", - ], - }, - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/package.json", - "path": "/packages/kbn-pm/src/commands", - "productionDependencies": Object { - "bar": "1.0.0", - }, - "scripts": Object {}, - "targetLocation": "/packages/kbn-pm/src/commands/target", - "version": "1.0.0", - }, - "bar" => Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - }, - Map { - "kibana" => Array [ - Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - ], - "bar" => Array [], - }, - ], -] -`; - -exports[`calls "kbn:bootstrap" scripts and links executables after installing deps: script 1`] = ` -Array [ - Array [ - Object { - "args": Array [], - "debug": undefined, - "pkg": Project { - "allDependencies": Object {}, - "devDependencies": Object {}, - "isWorkspaceProject": false, - "isWorkspaceRoot": false, - "json": Object { - "name": "bar", - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "version": "1.0.0", - }, - "nodeModulesLocation": "/packages/kbn-pm/src/commands/packages/bar/node_modules", - "packageJsonLocation": "/packages/kbn-pm/src/commands/packages/bar/package.json", - "path": "/packages/kbn-pm/src/commands/packages/bar", - "productionDependencies": Object {}, - "scripts": Object { - "kbn:bootstrap": "node ./bar.js", - }, - "targetLocation": "/packages/kbn-pm/src/commands/packages/bar/target", - "version": "1.0.0", - }, - "script": "kbn:bootstrap", - }, - ], -] -`; - -exports[`does not run installer if no deps in package: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], -] -`; - -exports[`handles "frozen-lockfile": install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [ - "--frozen-lockfile", - ], - ], -] -`; - -exports[`handles dependencies of dependencies: install in dir 1`] = ` -Array [ - Array [ - "/packages/kbn-pm/src/commands", - Array [], - ], - Array [ - "/packages/kbn-pm/src/commands/packages/bar", - Array [], - ], - Array [ - "/packages/kbn-pm/src/commands/packages/foo", - Array [], - ], -] -`; diff --git a/packages/kbn-pm/src/commands/bootstrap.test.ts b/packages/kbn-pm/src/commands/bootstrap.test.ts deleted file mode 100644 index 956c4e72933ba..0000000000000 --- a/packages/kbn-pm/src/commands/bootstrap.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -jest.mock('../utils/scripts'); -jest.mock('../utils/link_project_executables'); -jest.mock('../utils/validate_yarn_lock'); - -import { resolve } from 'path'; - -import { ToolingLogCollectingWriter } from '@kbn/dev-utils/tooling_log'; - -import { absolutePathSnapshotSerializer, stripAnsiSnapshotSerializer } from '../test_helpers'; -import { linkProjectExecutables } from '../utils/link_project_executables'; -import { IPackageJson } from '../utils/package_json'; -import { Project } from '../utils/project'; -import { buildProjectGraph } from '../utils/projects'; -import { installInDir, runScriptInPackageStreaming, yarnWorkspacesInfo } from '../utils/scripts'; -import { BootstrapCommand } from './bootstrap'; -import { Kibana } from '../utils/kibana'; -import { log } from '../utils/log'; - -const mockInstallInDir = installInDir as jest.Mock; -const mockRunScriptInPackageStreaming = runScriptInPackageStreaming as jest.Mock; -const mockLinkProjectExecutables = linkProjectExecutables as jest.Mock; -const mockYarnWorkspacesInfo = yarnWorkspacesInfo as jest.Mock; - -const logWriter = new ToolingLogCollectingWriter('debug'); -log.setLogLevel('silent'); -log.setWriters([logWriter]); -beforeEach(() => { - logWriter.messages.length = 0; -}); - -const createProject = (packageJson: IPackageJson, path = '.') => { - const project = new Project( - { - name: 'kibana', - version: '1.0.0', - ...packageJson, - }, - resolve(__dirname, path) - ); - - if (packageJson.workspaces) { - project.isWorkspaceRoot = true; - } - - return project; -}; -expect.addSnapshotSerializer(absolutePathSnapshotSerializer); -expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); - -beforeEach(() => { - mockYarnWorkspacesInfo.mockResolvedValue({}); -}); - -afterEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); -}); - -test('handles dependencies of dependencies', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - const foo = createProject( - { - dependencies: { - bar: 'link:../bar', - }, - name: 'foo', - }, - 'packages/foo' - ); - const bar = createProject( - { - dependencies: { - baz: 'link:../baz', - }, - name: 'bar', - }, - 'packages/bar' - ); - const baz = createProject( - { - name: 'baz', - }, - 'packages/baz' - ); - - const projects = new Map([ - ['kibana', kibana], - ['foo', foo], - ['bar', bar], - ['baz', baz], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - info [bar] running yarn, - "", - "", - info [foo] running yarn, - "", - "", - ] - `); -}); - -test('does not run installer if no deps in package', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - // bar has no dependencies - const bar = createProject( - { - name: 'bar', - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); - expect(logWriter.messages).toMatchInlineSnapshot(` - Array [ - info [kibana] running yarn, - "", - "", - ] - `); -}); - -test('handles "frozen-lockfile"', async () => { - const kibana = createProject({ - dependencies: { - foo: '2.2.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - - const projects = new Map([['kibana', kibana]]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: { - 'frozen-lockfile': true, - }, - rootPath: '', - kbn, - }); - - expect(mockInstallInDir.mock.calls).toMatchSnapshot('install in dir'); -}); - -test('calls "kbn:bootstrap" scripts and links executables after installing deps', async () => { - const kibana = createProject({ - dependencies: { - bar: '1.0.0', - }, - workspaces: { - packages: ['packages/*'], - }, - }); - const bar = createProject( - { - name: 'bar', - scripts: { - 'kbn:bootstrap': 'node ./bar.js', - }, - }, - 'packages/bar' - ); - - const projects = new Map([ - ['kibana', kibana], - ['bar', bar], - ]); - const kbn = new Kibana(projects); - const projectGraph = buildProjectGraph(projects); - - await BootstrapCommand.run(projects, projectGraph, { - extraArgs: [], - options: {}, - rootPath: '', - kbn, - }); - - expect(mockLinkProjectExecutables.mock.calls).toMatchSnapshot('link bins'); - expect(mockRunScriptInPackageStreaming.mock.calls).toMatchSnapshot('script'); -}); diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 7cf89c5f08f96..0fa3f355ae9d6 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -58,7 +58,9 @@ export const BootstrapCommand: ICommand = { const yarnLock = await readYarnLock(kbn); - await validateYarnLock(kbn, yarnLock); + if (options.validate) { + await validateYarnLock(kbn, yarnLock); + } await linkProjectExecutables(projects, projectGraph); diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts index 23dcb302f090d..22b9713b78332 100644 --- a/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.test.ts @@ -35,7 +35,8 @@ it('extracts expected components from html', () => { ## Release Note: Checkout this feature - `) + `), + 'release note' ) ).toMatchInlineSnapshot(`"Checkout this feature"`); @@ -46,10 +47,11 @@ it('extracts expected components from html', () => { Fixes: #1234 - #### Release Note: + #### Dev docs: We fixed an issue - `) + `), + 'dev docs' ) ).toMatchInlineSnapshot(`"We fixed an issue"`); @@ -60,8 +62,9 @@ it('extracts expected components from html', () => { Fixes: #1234 - Release note: Checkout feature foo - `) + OTHER TITLE: Checkout feature foo + `), + 'other title' ) ).toMatchInlineSnapshot(`"Checkout feature foo"`); @@ -73,7 +76,8 @@ it('extracts expected components from html', () => { My PR description release note : bar - `) + `), + 'release note' ) ).toMatchInlineSnapshot(`"bar"`); }); diff --git a/packages/kbn-release-notes/src/lib/get_note_from_description.ts b/packages/kbn-release-notes/src/lib/get_note_from_description.ts index 57df203470a5a..0d9135c431e36 100644 --- a/packages/kbn-release-notes/src/lib/get_note_from_description.ts +++ b/packages/kbn-release-notes/src/lib/get_note_from_description.ts @@ -19,11 +19,12 @@ import cheerio from 'cheerio'; -export function getNoteFromDescription(descriptionHtml: string) { +export function getNoteFromDescription(descriptionHtml: string, header: string) { + const re = new RegExp(`^(\\s*${header.toLowerCase()}(?:s)?\\s*:?\\s*)`, 'i'); const $ = cheerio.load(descriptionHtml); for (const el of $('p,h1,h2,h3,h4,h5').toArray()) { const text = $(el).text(); - const match = text.match(/^(\s*release note(?:s)?\s*:?\s*)/i); + const match = text.match(re); if (!match) { continue; diff --git a/packages/kbn-release-notes/src/lib/pr_api.ts b/packages/kbn-release-notes/src/lib/pr_api.ts index 1f26aa7ad86c3..5fa3dfdba10ef 100644 --- a/packages/kbn-release-notes/src/lib/pr_api.ts +++ b/packages/kbn-release-notes/src/lib/pr_api.ts @@ -178,7 +178,9 @@ export class PrApi { versions: labels .map((l) => Version.fromLabel(l)) .filter((v): v is Version => v instanceof Version), - note: getNoteFromDescription(node.bodyHTML), + note: + getNoteFromDescription(node.bodyHTML, 'release note') || + getNoteFromDescription(node.bodyHTML, 'dev docs'), }; } diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts index 85fb84c714e20..48c4d73c291fe 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.test.ts @@ -148,6 +148,17 @@ describe('getDescriptor', () => { }); }); + it('serializes RecordWithKnownAllProps', () => { + const usageInterface = usageInterfaces.get('RecordWithKnownAllProps')!; + const descriptor = getDescriptor(usageInterface, tsProgram); + expect(descriptor).toEqual({ + prop1: { kind: ts.SyntaxKind.NumberKeyword, type: 'NumberKeyword' }, + prop2: { kind: ts.SyntaxKind.NumberKeyword, type: 'NumberKeyword' }, + prop3: { kind: ts.SyntaxKind.NumberKeyword, type: 'NumberKeyword' }, + prop4: { kind: ts.SyntaxKind.NumberKeyword, type: 'NumberKeyword' }, + }); + }); + it('serializes IndexedAccessType', () => { const usageInterface = usageInterfaces.get('IndexedAccessType')!; const descriptor = getDescriptor(usageInterface, tsProgram); diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index ea5f184008026..d380e17759e21 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -88,7 +88,11 @@ export function getConstraints(node: ts.Node, program: ts.Program): any { if (ts.isUnionTypeNode(node)) { const types = node.types.filter(discardNullOrUndefined); - return types.map((typeNode) => getConstraints(typeNode, program)); + return types.reduce((acc, typeNode) => { + const constraints = getConstraints(typeNode, program); + const contraintsArray = Array.isArray(constraints) ? constraints : [constraints]; + return [...acc, ...contraintsArray]; + }, []); } if (ts.isLiteralTypeNode(node) && ts.isLiteralExpression(node.literal)) { diff --git a/packages/kbn-ui-framework/package.json b/packages/kbn-ui-framework/package.json index 639d4e17d0e71..21d25311420ca 100644 --- a/packages/kbn-ui-framework/package.json +++ b/packages/kbn-ui-framework/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.11.6", - "@elastic/eui": "29.0.0", + "@elastic/eui": "29.3.0", "@kbn/babel-preset": "1.0.0", "@kbn/optimizer": "1.0.0", "babel-loader": "^8.0.6", diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 69344174a2dc6..d74b45f973eb1 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -35,7 +35,6 @@ export const MonacoBarePluginApi = require('@kbn/monaco').BarePluginApi; export const React = require('react'); export const ReactDom = require('react-dom'); export const ReactDomServer = require('react-dom/server'); -export const ReactIntl = require('react-intl'); export const ReactRouter = require('react-router'); // eslint-disable-line export const ReactRouterDom = require('react-router-dom'); export const StyledComponents = require('styled-components'); @@ -54,5 +53,8 @@ export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); +// runtime deps which don't need to be copied across all bundles +export const TsLib = require('tslib'); + import * as Theme from './theme.ts'; export { Theme }; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index a5d6954fd5cc0..8f931fae4f337 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -39,7 +39,6 @@ exports.externals = { react: '__kbnSharedDeps__.React', 'react-dom': '__kbnSharedDeps__.ReactDom', 'react-dom/server': '__kbnSharedDeps__.ReactDomServer', - 'react-intl': '__kbnSharedDeps__.ReactIntl', 'react-router': '__kbnSharedDeps__.ReactRouter', 'react-router-dom': '__kbnSharedDeps__.ReactRouterDom', 'styled-components': '__kbnSharedDeps__.StyledComponents', @@ -64,5 +63,10 @@ exports.externals = { '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', lodash: '__kbnSharedDeps__.Lodash', 'lodash/fp': '__kbnSharedDeps__.LodashFp', + + /** + * runtime deps which don't need to be copied across all bundles + */ + tslib: '__kbnSharedDeps__.TsLib', }; exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index d2a590d29947b..0a154c537fec1 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,8 +9,8 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "23.0.0", - "@elastic/eui": "29.0.0", + "@elastic/charts": "23.2.1", + "@elastic/eui": "29.3.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", "@kbn/monaco": "1.0.0", @@ -26,7 +26,6 @@ "moment-timezone": "^0.5.27", "react": "^16.12.0", "react-dom": "^16.12.0", - "react-intl": "^2.8.0", "react-is": "^16.8.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", @@ -34,11 +33,13 @@ "rxjs": "^6.5.5", "styled-components": "^5.1.0", "symbol-observable": "^1.2.0", + "tslib": "^2.0.0", "whatwg-fetch": "^3.0.0" }, "devDependencies": { "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "css-loader": "^3.4.2", "del": "^5.1.0", "loader-utils": "^1.2.3", diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index b7d4e929ac93f..986ddba209270 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -77,6 +77,25 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ }, ], }, + { + test: !dev ? /[\\\/]@elastic[\\\/]eui[\\\/].*\.js$/ : () => false, + use: [ + { + loader: 'babel-loader', + options: { + plugins: [ + [ + require.resolve('babel-plugin-transform-react-remove-prop-types'), + { + mode: 'remove', + removeImport: true, + }, + ], + ], + }, + }, + ], + }, ], }, diff --git a/rfcs/images/background_sessions_client.png b/rfcs/images/background_sessions_client.png new file mode 100644 index 0000000000000..46cb90c93aa32 Binary files /dev/null and b/rfcs/images/background_sessions_client.png differ diff --git a/rfcs/images/background_sessions_server.png b/rfcs/images/background_sessions_server.png new file mode 100644 index 0000000000000..593db3156f879 Binary files /dev/null and b/rfcs/images/background_sessions_server.png differ diff --git a/rfcs/text/0013_background_sessions.md b/rfcs/text/0013_background_sessions.md new file mode 100644 index 0000000000000..056149e770448 --- /dev/null +++ b/rfcs/text/0013_background_sessions.md @@ -0,0 +1,489 @@ +- Start Date: (fill me in with today's date, YYYY-MM-DD) +- RFC PR: (leave this empty) +- Kibana Issue: (leave this empty) + +- Architecture diagram: https://app.lucidchart.com/documents/edit/cf35b512-616a-4734-bc72-43dde70dbd44/0_0 +- Mockups: https://www.figma.com/proto/FD2M7MUpLScJKOyYjfbmev/ES-%2F-Query-Management-v4?node-id=440%3A1&viewport=984%2C-99%2C0.09413627535104752&scaling=scale-down +- Old issue: https://github.com/elastic/kibana/issues/53335 +- Background search roadmap: https://github.com/elastic/kibana/issues/61738 +- POC: https://github.com/elastic/kibana/pull/64641 + +# Summary + +Background Sessions will enable Kibana applications and solutions to start a group of related search requests (such as those coming from a single load of a dashboard or SIEM timeline), navigate away or close the browser, then retrieve the results when they have completed. + +# Basic example + +At its core, background sessions are enabled via several new APIs, that: +- Start a session, associating multiple search requests with a single entity +- Store the session (and continue search requests in the background) +- Restore the background session + +```ts +const searchService = dataPluginStart.search; + +if (appState.sessionId) { + // If we are restoring a session, set the session ID in the search service + searchService.session.restore(sessionId); +} else { + // Otherwise, start a new background session to associate our search requests + appState.sessionId = searchService.session.start(); +} + +// Search, passing in the generated session ID. +// If this is a new session, the `search_interceptor` will associate and keep track of the async search ID with the session ID. +// If this is a restored session, the server will immediately return saved results. +// In the case where there is no saved result for a given request, or if the results have expired, `search` will throw an error with a meaningful error code. +const request = buildKibanaRequest(...); +request.sessionId = searchService.session.get(); +const response$ = await searchService.search(request); + +// Calling `session.store()`, creates a saved object for this session, allowing the user to navigate away. +// The session object will be saved with all async search IDs that were executed so far. +// Any follow up searches executed with this sessionId will be saved into this object as well. +const backgroundSession = await searchService.session.store(); +``` + +# Motivation + +Kibana is great at providing fast results from large sets of "hot" data. However, there is an increasing number of use cases where users want to analyze large amounts of "colder" data (such as year-over-year reports, historical or audit data, batch queries, etc.). + +For these cases, users run into two limitations: + 1. Kibana has a default timeout of 30s per search. This is controlled by the `elasticsearch.requestTimeout` setting (originally intended to protect clusters from unintentional overload by a single query). + 2. Kibana cancels queries upon navigating away from an application, once again, as means of protecting clusters and reducing unnecessary load. + +In 7.7, with the introduction of the `_async_search` API in Elasticsearch, we provided Kibana users a way to bypass the timeout, but users still need to remain on-screen for the entire duration of the search requests. + +The primary motivation of this RFC is to enable users to do the following without needing to keep Kibana open, or while moving onto other work inside Kibana: + +- Run long search requests (beyond 30 seconds) +- View their status (complete/incomplete) +- Cancel incomplete search requests +- Retrieve completed search request results + +# Detailed design + +Because a single view (such as a dashboard with multiple visualizations) can initiate multiple search requests, we need a way to associate the search requests together in a single entity. + +We call this entity a `session`, and when a user decides that they want to continue running the search requests while moving onto other work, we will create a saved object corresponding with that specific `session`, persisting the *sessionId* along with a mapping of each *request's hash* to the *async ID* returned by Elasticsearch. + +## High Level Flow Charts + +### Client side search + +This diagram matches any case where `data.search` is called from the front end: + +![image](../images/background_sessions_client.png) + +### Server side search + +This case happens if the server is the one to invoke the `data.search` endpoint, for example with TSVB. + +![image](../images/background_sessions_server.png) + +## Data and Saved Objects + +### Background Session Status + +```ts +export enum BackgroundSessionStatus { + Running, // The session has at least one running search ID associated with it. + Done, // All search IDs associated with this session have completed. + Error, // At least one search ID associated with this session had an error. + Expired, // The session has expired. Associated search ID data was cleared from ES. +} +``` + +### Saved Object Structure + +The saved object created for a background session will be scoped to a single space, and will be a `hidden` saved object +(so that it doesn't show in the management listings). We will provide a separate interface for users to manage their own +background sessions (which will use the `list`, `expire`, and `extend` methods described below, which will be restricted +per-user). + +```ts +interface BackgroundSessionAttributes extends SavedObjectAttributes { + sessionId: string; + userId: string; // Something unique to the user who generated this session, like username/realm-name/realm-type + status: BackgroundSessionStatus; + name: string; + creation: Date; + expiration: Date; + idMapping: { [key: string]: string }; + url: string; // A URL relative to the Kibana root to retrieve the results of a completed background session (and/or to return to an incomplete view) + metadata: { [key: string]: any } // Any data the specific application requires to restore a background session view +} +``` + +The URL that is provided will need to be generated by the specific application implementing background sessions. We +recommend using the URL generator to ensure that URLs are backwards-compatible since background sessions may exist as +long as a user continues to extend the expiration. + +## Frontend Services + +Most sessions will probably not be saved. Therefore, to avoid creating unnecessary saved objects, the browser will keep track of requests and their respective search IDs, until the user chooses to store the session. Once a session is stored, any additional searches will be immediately saved on the server side. + +### New Session Service + +We will expose a new frontend `session` service on the `data` plugin `search` service. + +The service will expose the following APIs: + +```ts +interface ISessionService { + /** + * Returns the current session ID + */ + getActiveSessionId: () => string; + + /** + * Sets the current session + * @param sessionId: The ID of the session to set + * @param isRestored: Whether or not the session is being restored + */ + setActiveSessionId: (sessionId: string, isRestored: boolean) => void; + + /** + * Start a new session, by generating a new session ID (calls `setActiveSessionId` internally) + */ + start: () => string; + + /** + * Store a session, alongside with any tracked searchIds. + * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. + * @param name A display name for the session. + * @param url TODO: is the URL provided here? How? + * @returns The stored `BackgroundSessionAttributes` object + * @throws Throws an error in OSS. + */ + store: (sessionId: string, name: string, url: string) => Promise + + /** + * @returns Is the current session stored (i.e. is there a saved object corresponding with this sessionId). + */ + isStored: () => boolean; + + /** + * @returns Is the current session a restored session + */ + isRestored: () => boolean; + + /** + * Mark a session and and all associated searchIds as expired. + * Cancels active requests, if there are any. + * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. + * @returns success status + * @throws Throws an error in OSS. + */ + expire: (sessionId: string) => Promise + + /** + * Extend a session and all associated searchIds. + * @param sessionId Session ID to extend. Probably retrieved from `sessionService.get()`. + * @param extendBy Time to extend by, can be a relative or absolute string. + * @returns success status + * @throws Throws an error in OSS. + */ + extend: (sessionId: string, extendBy: string)=> Promise + + /** + * @param sessionId the ID of the session to retrieve the saved object. + * @returns a filtered list of BackgroundSessionAttributes objects. + * @throws Throws an error in OSS. + */ + get: (sessionId: string) => Promise + + /** + * @param options The options to query for specific background session saved objects. + * @returns a filtered list of BackgroundSessionAttributes objects. + * @throws Throws an error in OSS. + */ + list: (options: SavedObjectsFindOptions) => Promise + + /** + * Clears out any session info as well as the current session. Called internally whenever the user navigates + * between applications. + * @internal + */ + clear: () => void; + + /** + * Track a search ID of a sessionId, if it exists. Called internally by the search service. + * @param sessionId + * @param request + * @param searchId + * @internal + */ + trackSearchId: ( + sessionId: string, + request: IKibanaSearchRequest, + searchId: string, + ) => Promise +} +``` + +## Backend Services and Routes + +The server side's feature implementation builds on how Elasticsearch's `async_search` endpoint works. When making an +initial new request to Elasticsearch, it returns a search ID that can be later used to retrieve the results. + +The server will then store that `request`, `sessionId`, and `searchId` in a mapping in memory, and periodically query +for a saved object corresponding with that session. If the saved object is found, it will update the saved object to +include this `request`/`searchId` combination, and remove it from memory. If, after a period of time (5 minutes?) the +saved object has not been found, we will stop polling for that `sessionId` and remove the `request`/`searchId` from +memory. + +When the server receives a search request that has a `sessionId` and is marked as a `restore` request, the server will +attempt to find the correct id within the saved object, and use it to retrieve the results previously saved. + +### New Session Service + +```ts +interface ISessionService { + /** + * Adds a search ID to a Background Session, if it exists. + * Also extends the expiration of the search ID to match the session's expiration. + * @param request + * @param sessionId + * @param searchId + * @returns true if id was added, false if Background Session doesn't exist or if there was an error while updating. + * @throws an error if `searchId` already exists in the mapping for this `sessionId` + */ + trackSearchId: ( + request: KibanaRequest, + sessionId: string, + searchId: string, + ) => Promise + + /** + * Get a Background Session object. + * @param request + * @param sessionId + * @returns the Background Session object if exists, or undefined. + */ + get: async ( + request: KibanaRequest, + sessionId: string + ) => Promise + + /** + * Get a searchId from a Background Session object. + * @param request + * @param sessionId + * @returns the searchID if exists on the Background Session, or undefined. + */ + getSearchId: async ( + request: KibanaRequest, + sessionId: string + ) => Promise + + /** + * Store a session. + * @param request + * @param sessionId Session ID to store. Probably retrieved from `sessionService.get()`. + * @param searchIdMap A mapping of hashed requests mapped to the corresponding searchId. + * @param url TODO: is the URL provided here? How? + * @returns The stored `BackgroundSessionAttributes` object + * @throws Throws an error in OSS. + * @internal (Consumers should use searchInterceptor.sendToBackground()) + */ + store: ( + request: KibanaRequest, + sessionId: string, + name: string, + url: string, + searchIdMapping?: Record + ) => Promise + + /** + * Mark a session as and all associated searchIds as expired. + * @param request + * @param sessionId + * @returns success status + * @throws Throws an error in OSS. + */ + expire: async ( + request: KibanaRequest, + sessionId: string + ) => Promise + + /** + * Extend a session and all associated searchIds. + * @param request + * @param sessionId + * @param extendBy Time to extend by, can be a relative or absolute string. + * @returns success status + * @throws Throws an error in OSS. + */ + extend: async ( + request: KibanaRequest, + sessionId: string, + extendBy: string, + ) => Promise + + /** + * Get a list of background session objects. + * @param request + * @param sessionId + * @returns success status + * @throws Throws an error in OSS. + */ + list: async ( + request: KibanaRequest, + ) => Promise + + /** + * Update the status of a given session + * @param request + * @param sessionId + * @param status + * @returns success status + * @throws Throws an error in OSS. + */ + updateStatus: async ( + request: KibanaRequest, + sessionId: string, + status: BackgroundSessionStatus + ) => Promise +} + +``` + +### Search Service Changes + +There are cases where search requests are issued by the server (Like TSVB). +We can simplify this flow by introducing a mechanism, similar to the frontend one, tracking the information in memory and polling for a saved object with a corresponding sessionId to store the ids into it. + +```ts +interface SearchService { + /** + * The search API will accept the option `trackId`, which will track the search ID, if available, on the server, until a corresponding saved object is created. + **/ + search: async ( + context: RequestHandlerContext, + request: IEnhancedEsSearchRequest, + options?: ISearchOptions + ) => ISearchResponse +} +``` + +### Server Routes + +Each route exposes the corresponding method from the Session Service (used only by the client-side service, not meant to be used directly by any consumers): + +`POST /internal/session/store` + +`POST /internal/session/extend` + +`POST /internal/session/expire` + +`GET /internal/session/list` + +### Search Strategy Integration + +If the `EnhancedEsSearchStrategy` receives a `restore` option, it will attempt reloading data using the Background Session saved object matching the provided `sessionId`. If there are any errors during that process, the strategy will return an error response and *not attempt to re-run the request. + +The strategy will track the asyncId on the server side, if `trackId` option is provided. + +### Monitoring Service + +The `data` plugin will register a task with the task manager, periodically monitoring the status of incomplete background sessions. + +It will query the list of all incomplete sessions, and check the status of each search that is executing. If the search requests are all complete, it will update the corresponding saved object to have a `status` of `complete`. If any of the searches return an error, it will update the saved object to an `error` state. If the search requests have expired, it will update the saved object to an `expired` state. Expired sessions will be purged once they are older than the time definedby the `EXPIRED_SESSION_TTL` advanced setting. + +Once there's a notification area in Kibana, we may use that mechanism to push completion \ error notifications to the client. + +## Miscellaneous + +#### Relative dates and restore URLs + +Restoring a sessionId depends on each request's `sha-256` hash matching exactly to the ones saved, requiring special attention to relative date ranges, as having these might yield ambiguous results. + +There are two potential scenarios: + - A relative date (for example `now-1d`) is being used in query DSL - In this case any future hash will match, but the returned data *won't match the displayed timeframe*. For example, a report might state that it shows data from yesterday, but actually show data from a week ago. + - A relative date is being translated by the application before being set to the query DSL - In this case a different date will be sent and the hash will never match, resulting in an error restoring the dashboard. + +Both scenarios require careful attention during the UI design and implementation. + +The former can be resolved by clearly displaying the creation time of the restored Background Session. We could also attempt translating relative dates to absolute one's, but this might be challenging as relative dates may appear deeply nested within the DSL. + +The latter case happens at the moment for the timepicker only: The relative date is being translated each time into an absolute one, before being sent to Elasticsearch. In order to avoid issues, we'll have to make sure that restore URLs are generated with an absolute date, to make sure they are restored correctly. + +#### Changing a restored session + +If you have restored a Background Session, making any type of change to it (time range, filters, etc.) will trigger new (potentially long) searches. There should be a clear indication in the UI that the data is no longer stored. A user then may choose to send it to background, resulting in a new Background Session being saved. + +#### Loading an errored \ expired \ canceled session + +When trying to restore a Background Session, if any of the requests hashes don't match the ones saved, or if any of the saved async search IDs are expired, a meaningful error code will be returned by the server **by those requests**. It is each application's responsibility to handle these errors appropriately. + +In such a scenario, the session will be partially restored. + +#### Extending Expiration + +Sessions are given an expiration date defined in an advanced setting (5 days by default). This expiration date is measured from the time the Background Session is saved, and it includes the time it takes to generate the results. + +A session's expiration date may be extended indefinitely. However, if a session was canceled or has already expired, it needs to be re-run. + +# Limitations + +In the first iteration, cases which require multiple search requests to be made serially will not be supported. The +following are examples of such scenarios: + +- When a visualization is configured with a terms agg with an "other" bucket +- When using blended layers or term joins in Maps + +Eventually, when expressions can be run on the server, they will run in the context of a specific `sessionId`, hence enabling those edge cases too. + +# Drawbacks + +One drawback of this approach is that we will be regularly polling Elasticsearch for saved objects, which will increase +load on the Elasticsearch server, in addition to the Kibana server (since all server-side processes share the same event +loop). We've opened https://github.com/elastic/kibana/issues/77293 to track this, and hopefully come up with benchmarks +so we feel comfortable moving forward with this approach. + +Two potential drawbacks stem from storing things in server memory. If a Kibana server is restarted, in-memory results +will be lost. (This can be an issue if a search request has started, and the user has sent to background, but the +background session saved object has not yet been updated with the search request ID.) In such cases, the user interface +will need to indicate errors for requests that were not stored in the saved object. + +There is also the consideration of the memory footprint of the Kibana server; however, since +we are only storing a hash of the request and search request ID, and are periodically cleaning it up (see Backend +Services and Routes), we do not anticipate the footprint to increase significantly. + +The results of search requests that have been sent to the background will be stored in Elasticsearch for several days, +even if they will only be retrieved once. This will be mitigated by allowing the user manually delete a background +session object after it has been accessed. + +# Alternatives + +What other designs have been considered? What is the impact of not doing this? + +# Adoption strategy + +(See "Basic example" above.) + +Any application or solution that uses the `data` plugin `search` services will be able to facilitate background sessions +fairly simply. The public side will need to create/clear sessions when appropriate, and ensure the `sessionId` is sent +with all search requests. It will also need to ensure that any necessary application data, as well as a `restoreUrl` is +sent when creating the saved object. + +The server side will just need to ensure that the `sessionId` is sent to the `search` service. If bypassing the `search` +service, it will need to also call `trackSearchId` when the first response is received, and `getSearchId` when restoring +the view. + +# How we teach this + +What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing Kibana patterns? + +Would the acceptance of this proposal mean the Kibana documentation must be +re-organized or altered? Does it change how Kibana is taught to new developers +at any level? + +How should this feature be taught to existing Kibana developers? + +# Unresolved questions + +Optional, but suggested for first drafts. What parts of the design are still +TBD? diff --git a/scripts/functional_tests.js b/scripts/functional_tests.js index 4facbe1ffbb07..2b338b1c054aa 100644 --- a/scripts/functional_tests.js +++ b/scripts/functional_tests.js @@ -23,6 +23,7 @@ const alwaysImportedTests = [ require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/ui_capabilities/newsfeed_err/config.ts'), require.resolve('../test/new_visualize_flow/config.js'), + require.resolve('../test/security_functional/config.ts'), ]; // eslint-disable-next-line no-restricted-syntax const onlyNotInCoverageTests = [ diff --git a/scripts/kibana_keystore.js b/scripts/kibana_keystore.js index 3efd3500e1f8e..3f970a6517225 100644 --- a/scripts/kibana_keystore.js +++ b/scripts/kibana_keystore.js @@ -17,5 +17,4 @@ * under the License. */ -require('../src/setup_node_env'); -require('../src/cli_keystore'); +require('../src/cli_keystore/dev'); diff --git a/scripts/kibana_plugin.js b/scripts/kibana_plugin.js index 2478e515297ef..5196bdbb853f5 100644 --- a/scripts/kibana_plugin.js +++ b/scripts/kibana_plugin.js @@ -17,5 +17,4 @@ * under the License. */ -require('../src/setup_node_env'); -require('../src/cli_plugin/cli'); +require('../src/cli_plugin/dev'); diff --git a/src/apm.js b/src/apm.js index 8a0c010d993f1..bde37fa006c61 100644 --- a/src/apm.js +++ b/src/apm.js @@ -36,7 +36,22 @@ module.exports = function (serviceName = name) { apmConfig = loadConfiguration(process.argv, ROOT_DIR, isKibanaDistributable); const conf = apmConfig.getConfig(serviceName); - require('elastic-apm-node').start(conf); + const apm = require('elastic-apm-node'); + + // Filter out all user PII + apm.addFilter((payload) => { + try { + if (payload.context && payload.context.user && typeof payload.context.user === 'object') { + Object.keys(payload.context.user).forEach((key) => { + payload.context.user[key] = '[REDACTED]'; + }); + } + } finally { + return payload; + } + }); + + apm.start(conf); }; module.exports.getConfig = (serviceName) => { @@ -50,4 +65,3 @@ module.exports.getConfig = (serviceName) => { } return {}; }; -module.exports.isKibanaDistributable = isKibanaDistributable; diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index 3d81185e8a313..931650a67687c 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -110,6 +110,7 @@ export class ClusterManager { type: 'server', log: this.log, argv: serverArgv, + apmServiceName: 'kibana', })), ]; diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts index f6205b41ac5a5..d28065765070b 100644 --- a/src/cli/cluster/worker.ts +++ b/src/cli/cluster/worker.ts @@ -49,6 +49,7 @@ interface WorkerOptions { title?: string; watch?: boolean; baseArgv?: string[]; + apmServiceName?: string; } export class Worker extends EventEmitter { @@ -89,6 +90,7 @@ export class Worker extends EventEmitter { NODE_OPTIONS: process.env.NODE_OPTIONS || '', kbnWorkerType: this.type, kbnWorkerArgv: JSON.stringify([...(opts.baseArgv || baseArgv), ...(opts.argv || [])]), + ELASTIC_APM_SERVICE_NAME: opts.apmServiceName || '', }; } diff --git a/src/cli/dev.js b/src/cli/dev.js index 9d0cb35c3d730..a284c82dfeb6e 100644 --- a/src/cli/dev.js +++ b/src/cli/dev.js @@ -17,6 +17,6 @@ * under the License. */ -require('../apm')(process.env.ELASTIC_APM_PROXY_SERVICE_NAME || 'kibana-proxy'); +require('../apm')(process.env.ELASTIC_APM_SERVICE_NAME || 'kibana-proxy'); require('../setup_node_env'); require('./cli'); diff --git a/src/cli/dist.js b/src/cli/dist.js index 2e26eaf52e836..05f0a68aa495c 100644 --- a/src/cli/dist.js +++ b/src/cli/dist.js @@ -18,6 +18,5 @@ */ require('../apm')(); -require('../setup_node_env/no_transpilation'); -require('core-js/stable'); +require('../setup_node_env/dist'); require('./cli'); diff --git a/src/cli_keystore/index.js b/src/cli_keystore/dev.js similarity index 100% rename from src/cli_keystore/index.js rename to src/cli_keystore/dev.js diff --git a/src/cli_keystore/dist.js b/src/cli_keystore/dist.js new file mode 100644 index 0000000000000..60abe225372aa --- /dev/null +++ b/src/cli_keystore/dist.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../setup_node_env/dist'); +require('./cli_keystore'); diff --git a/src/cli_plugin/index.js b/src/cli_plugin/dev.js similarity index 100% rename from src/cli_plugin/index.js rename to src/cli_plugin/dev.js diff --git a/src/cli_plugin/dist.js b/src/cli_plugin/dist.js new file mode 100644 index 0000000000000..cc931f60db1e4 --- /dev/null +++ b/src/cli_plugin/dist.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../setup_node_env/dist'); +require('./cli'); diff --git a/src/core/public/apm_system.ts b/src/core/public/apm_system.ts index 0a18f02c97293..5e4953b96dc5b 100644 --- a/src/core/public/apm_system.ts +++ b/src/core/public/apm_system.ts @@ -28,6 +28,7 @@ import type { InternalApplicationStart } from './application'; interface ApmConfig { // AgentConfigOptions is not exported from @elastic/apm-rum + active?: boolean; globalLabels?: Record; } @@ -39,10 +40,10 @@ export class ApmSystem { private readonly enabled: boolean; /** * `apmConfig` would be populated with relevant APM RUM agent - * configuration if server is started with `ELASTIC_APM_ACTIVE=true` + * configuration if server is started with elastic.apm.* config. */ constructor(private readonly apmConfig?: ApmConfig) { - this.enabled = process.env.IS_KIBANA_DISTRIBUTABLE !== 'true' && apmConfig != null; + this.enabled = apmConfig != null && !!apmConfig.active; } async setup() { diff --git a/src/core/public/overlays/banners/banners_list.test.tsx b/src/core/public/overlays/banners/banners_list.test.tsx index dbee20790fa94..3850a88699d90 100644 --- a/src/core/public/overlays/banners/banners_list.test.tsx +++ b/src/core/public/overlays/banners/banners_list.test.tsx @@ -43,7 +43,7 @@ describe('BannersList', () => { ]); expect(mount().html()).toMatchInlineSnapshot( - `"

Hello!

"` + `"

Hello!

"` ); }); @@ -85,7 +85,7 @@ describe('BannersList', () => { // Two new banners should be rendered expect(component.html()).toMatchInlineSnapshot( - `"

First Banner!

Second banner!

"` + `"

First Banner!

Second banner!

"` ); // Original banner should be unmounted expect(unmount).toHaveBeenCalled(); diff --git a/src/core/public/overlays/banners/banners_list.tsx b/src/core/public/overlays/banners/banners_list.tsx index 6503af985f9c8..30acf6abff70b 100644 --- a/src/core/public/overlays/banners/banners_list.tsx +++ b/src/core/public/overlays/banners/banners_list.tsx @@ -59,6 +59,11 @@ const BannerItem: React.FunctionComponent<{ banner: OverlayBanner }> = ({ banner useEffect(() => banner.mount(element.current!), [banner]); // Only unmount / remount if banner object changed. return ( -
+
); }; diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts new file mode 100644 index 0000000000000..c443ce72f5ed7 --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -0,0 +1,153 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { BehaviorSubject } from 'rxjs'; +import { CoreUsageDataService } from './core_usage_data_service'; +import { CoreUsageData, CoreUsageDataStart } from './types'; + +const createStartContractMock = () => { + const startContract: jest.Mocked = { + getCoreUsageData: jest.fn().mockResolvedValue( + new BehaviorSubject({ + config: { + elasticsearch: { + apiVersion: 'master', + customHeadersConfigured: false, + healthCheckDelayMs: 2500, + logQueries: false, + numberOfHostsConfigured: 1, + pingTimeoutMs: 30000, + requestHeadersWhitelistConfigured: false, + requestTimeoutMs: 30000, + shardTimeoutMs: 30000, + sniffIntervalMs: -1, + sniffOnConnectionFault: false, + sniffOnStart: false, + ssl: { + alwaysPresentCertificate: false, + certificateAuthoritiesConfigured: false, + certificateConfigured: false, + keyConfigured: false, + verificationMode: 'full', + keystoreConfigured: false, + truststoreConfigured: false, + }, + }, + http: { + basePathConfigured: false, + compression: { + enabled: true, + referrerWhitelistConfigured: false, + }, + keepaliveTimeout: 120000, + maxPayloadInBytes: 1048576, + requestId: { + allowFromAnyIp: false, + ipAllowlistConfigured: false, + }, + rewriteBasePath: false, + socketTimeout: 120000, + ssl: { + certificateAuthoritiesConfigured: false, + certificateConfigured: false, + cipherSuites: [ + 'ECDHE-RSA-AES128-GCM-SHA256', + 'ECDHE-ECDSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES256-GCM-SHA384', + 'ECDHE-ECDSA-AES256-GCM-SHA384', + 'DHE-RSA-AES128-GCM-SHA256', + 'ECDHE-RSA-AES128-SHA256', + 'DHE-RSA-AES128-SHA256', + 'ECDHE-RSA-AES256-SHA384', + 'DHE-RSA-AES256-SHA384', + 'ECDHE-RSA-AES256-SHA256', + 'DHE-RSA-AES256-SHA256', + 'HIGH', + '!aNULL', + '!eNULL', + '!EXPORT', + '!DES', + '!RC4', + '!MD5', + '!PSK', + '!SRP', + '!CAMELLIA', + ], + clientAuthentication: 'none', + keyConfigured: false, + keystoreConfigured: false, + redirectHttpFromPortConfigured: false, + supportedProtocols: ['TLSv1.1', 'TLSv1.2'], + truststoreConfigured: false, + }, + xsrf: { + disableProtection: false, + whitelistConfigured: false, + }, + }, + logging: { + appendersTypesUsed: [], + loggersConfiguredCount: 0, + }, + savedObjects: { + maxImportExportSizeBytes: 10000, + maxImportPayloadBytes: 10485760, + }, + }, + environment: { + memory: { + heapSizeLimit: 1, + heapTotalBytes: 1, + heapUsedBytes: 1, + }, + }, + services: { + savedObjects: { + indices: [ + { + docsCount: 1, + docsDeleted: 1, + alias: 'test_index', + primaryStoreSizeBytes: 1, + storeSizeBytes: 1, + }, + ], + }, + }, + }) + ), + }; + + return startContract; +}; + +const createMock = () => { + const mocked: jest.Mocked> = { + setup: jest.fn(), + start: jest.fn().mockReturnValue(createStartContractMock()), + stop: jest.fn(), + }; + return mocked; +}; + +export const coreUsageDataServiceMock = { + create: createMock, + createStartContract: createStartContractMock, +}; diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts new file mode 100644 index 0000000000000..a664f6514e9c8 --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -0,0 +1,259 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { BehaviorSubject, Observable } from 'rxjs'; +import { HotObservable } from 'rxjs/internal/testing/HotObservable'; +import { TestScheduler } from 'rxjs/testing'; + +import { configServiceMock } from '../config/mocks'; + +import { mockCoreContext } from '../core_context.mock'; +import { config as RawElasticsearchConfig } from '../elasticsearch/elasticsearch_config'; +import { config as RawHttpConfig } from '../http/http_config'; +import { config as RawLoggingConfig } from '../logging/logging_config'; +import { config as RawKibanaConfig } from '../kibana_config'; +import { savedObjectsConfig as RawSavedObjectsConfig } from '../saved_objects/saved_objects_config'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; +import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service.mock'; + +import { CoreUsageDataService } from './core_usage_data_service'; +import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; + +describe('CoreUsageDataService', () => { + const getTestScheduler = () => + new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + let service: CoreUsageDataService; + const configService = configServiceMock.create(); + configService.atPath.mockImplementation((path) => { + if (path === 'elasticsearch') { + return new BehaviorSubject(RawElasticsearchConfig.schema.validate({})); + } else if (path === 'server') { + return new BehaviorSubject(RawHttpConfig.schema.validate({})); + } else if (path === 'logging') { + return new BehaviorSubject(RawLoggingConfig.schema.validate({})); + } else if (path === 'savedObjects') { + return new BehaviorSubject(RawSavedObjectsConfig.schema.validate({})); + } else if (path === 'kibana') { + return new BehaviorSubject(RawKibanaConfig.schema.validate({})); + } + return new BehaviorSubject({}); + }); + const coreContext = mockCoreContext.create({ configService }); + + beforeEach(() => { + service = new CoreUsageDataService(coreContext); + }); + + describe('start', () => { + describe('getCoreUsageData', () => { + it('returns core metrics for default config', () => { + const metrics = metricsServiceMock.createInternalSetupContract(); + service.setup({ metrics }); + const elasticsearch = elasticsearchServiceMock.createStart(); + elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ + body: [ + { + name: '.kibana_task_manager_1', + 'docs.count': 10, + 'docs.deleted': 10, + 'store.size': 1000, + 'pri.store.size': 2000, + }, + ], + } as any); + elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ + body: [ + { + name: '.kibana_1', + 'docs.count': 20, + 'docs.deleted': 20, + 'store.size': 2000, + 'pri.store.size': 4000, + }, + ], + } as any); + const typeRegistry = savedObjectsServiceMock.createTypeRegistryMock(); + typeRegistry.getAllTypes.mockReturnValue([ + { name: 'type 1', indexPattern: '.kibana' }, + { name: 'type 2', indexPattern: '.kibana_task_manager' }, + ] as any); + + const { getCoreUsageData } = service.start({ + savedObjects: savedObjectsServiceMock.createInternalStartContract(typeRegistry), + elasticsearch, + }); + expect(getCoreUsageData()).resolves.toMatchInlineSnapshot(` + Object { + "config": Object { + "elasticsearch": Object { + "apiVersion": "master", + "customHeadersConfigured": false, + "healthCheckDelayMs": 2500, + "logQueries": false, + "numberOfHostsConfigured": 1, + "pingTimeoutMs": 30000, + "requestHeadersWhitelistConfigured": false, + "requestTimeoutMs": 30000, + "shardTimeoutMs": 30000, + "sniffIntervalMs": -1, + "sniffOnConnectionFault": false, + "sniffOnStart": false, + "ssl": Object { + "alwaysPresentCertificate": false, + "certificateAuthoritiesConfigured": false, + "certificateConfigured": false, + "keyConfigured": false, + "keystoreConfigured": false, + "truststoreConfigured": false, + "verificationMode": "full", + }, + }, + "http": Object { + "basePathConfigured": false, + "compression": Object { + "enabled": true, + "referrerWhitelistConfigured": false, + }, + "keepaliveTimeout": 120000, + "maxPayloadInBytes": 1048576, + "requestId": Object { + "allowFromAnyIp": false, + "ipAllowlistConfigured": false, + }, + "rewriteBasePath": false, + "socketTimeout": 120000, + "ssl": Object { + "certificateAuthoritiesConfigured": false, + "certificateConfigured": false, + "cipherSuites": Array [ + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "DHE-RSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-SHA256", + "DHE-RSA-AES128-SHA256", + "ECDHE-RSA-AES256-SHA384", + "DHE-RSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA256", + "DHE-RSA-AES256-SHA256", + "HIGH", + "!aNULL", + "!eNULL", + "!EXPORT", + "!DES", + "!RC4", + "!MD5", + "!PSK", + "!SRP", + "!CAMELLIA", + ], + "clientAuthentication": "none", + "keyConfigured": false, + "keystoreConfigured": false, + "redirectHttpFromPortConfigured": false, + "supportedProtocols": Array [ + "TLSv1.1", + "TLSv1.2", + ], + "truststoreConfigured": false, + }, + "xsrf": Object { + "disableProtection": false, + "whitelistConfigured": false, + }, + }, + "logging": Object { + "appendersTypesUsed": Array [], + "loggersConfiguredCount": 0, + }, + "savedObjects": Object { + "maxImportExportSizeBytes": 10000, + "maxImportPayloadBytes": 10485760, + }, + }, + "environment": Object { + "memory": Object { + "heapSizeLimit": 1, + "heapTotalBytes": 1, + "heapUsedBytes": 1, + }, + }, + "services": Object { + "savedObjects": Object { + "indices": Array [ + Object { + "alias": ".kibana", + "docsCount": 10, + "docsDeleted": 10, + "primaryStoreSizeBytes": 2000, + "storeSizeBytes": 1000, + }, + Object { + "alias": ".kibana_task_manager", + "docsCount": 20, + "docsDeleted": 20, + "primaryStoreSizeBytes": 4000, + "storeSizeBytes": 2000, + }, + ], + }, + }, + } + `); + }); + }); + }); + + describe('setup and stop', () => { + it('subscribes and unsubscribes from all config paths and metrics', () => { + getTestScheduler().run(({ cold, hot, expectSubscriptions }) => { + const observables: Array> = []; + configService.atPath.mockImplementation(() => { + const newObservable = hot('-a-------'); + observables.push(newObservable); + return newObservable; + }); + const metrics = metricsServiceMock.createInternalSetupContract(); + metrics.getOpsMetrics$.mockImplementation(() => { + const newObservable = hot('-a-------'); + observables.push(newObservable); + return newObservable as Observable; + }); + + service.setup({ metrics }); + + // Use the stopTimer$ to delay calling stop() until the third frame + const stopTimer$ = cold('---a|'); + stopTimer$.subscribe(() => { + service.stop(); + }); + + const subs = '^--!'; + + observables.forEach((o) => { + expectSubscriptions(o.subscriptions).toBe(subs); + }); + }); + }); + }); +}); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts new file mode 100644 index 0000000000000..f729e23cb68bc --- /dev/null +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -0,0 +1,285 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { CoreService } from 'src/core/types'; +import { SavedObjectsServiceStart } from 'src/core/server'; +import { CoreContext } from '../core_context'; +import { ElasticsearchConfigType } from '../elasticsearch/elasticsearch_config'; +import { HttpConfigType } from '../http'; +import { LoggingConfigType } from '../logging'; +import { SavedObjectsConfigType } from '../saved_objects/saved_objects_config'; +import { CoreServicesUsageData, CoreUsageData, CoreUsageDataStart } from './types'; +import { isConfigured } from './is_configured'; +import { ElasticsearchServiceStart } from '../elasticsearch'; +import { KibanaConfigType } from '../kibana_config'; +import { MetricsServiceSetup, OpsMetrics } from '..'; + +export interface SetupDeps { + metrics: MetricsServiceSetup; +} + +export interface StartDeps { + savedObjects: SavedObjectsServiceStart; + elasticsearch: ElasticsearchServiceStart; +} + +/** + * Because users can configure their Saved Object to any arbitrary index name, + * we need to map customized index names back to a "standard" index name. + * + * e.g. If a user configures `kibana.index: .my_saved_objects` we want to the + * collected data to be grouped under `.kibana` not ".my_saved_objects". + * + * This is rather brittle, but the option to configure index names might go + * away completely anyway (see #60053). + * + * @param index The index name configured for this SO type + * @param kibanaConfigIndex The default kibana index as configured by the user + * with `kibana.index` + */ +const kibanaOrTaskManagerIndex = (index: string, kibanaConfigIndex: string) => { + return index === kibanaConfigIndex ? '.kibana' : '.kibana_task_manager'; +}; + +export class CoreUsageDataService implements CoreService { + private elasticsearchConfig?: ElasticsearchConfigType; + private configService: CoreContext['configService']; + private httpConfig?: HttpConfigType; + private loggingConfig?: LoggingConfigType; + private soConfig?: SavedObjectsConfigType; + private stop$: Subject; + private opsMetrics?: OpsMetrics; + private kibanaConfig?: KibanaConfigType; + + constructor(core: CoreContext) { + this.configService = core.configService; + this.stop$ = new Subject(); + } + + private async getSavedObjectIndicesUsageData( + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart + ): Promise { + const indices = await Promise.all( + Array.from( + savedObjects + .getTypeRegistry() + .getAllTypes() + .reduce((acc, type) => { + const index = type.indexPattern ?? this.kibanaConfig!.index; + return index != null ? acc.add(index) : acc; + }, new Set()) + .values() + ).map((index) => { + // The _cat/indices API returns the _index_ and doesn't return a way + // to map back from the index to the alias. So we have to make an API + // call for every alias + return elasticsearch.client.asInternalUser.cat + .indices({ + index, + format: 'JSON', + bytes: 'b', + }) + .then(({ body }) => { + const stats = body[0]; + return { + alias: kibanaOrTaskManagerIndex(index, this.kibanaConfig!.index), + docsCount: stats['docs.count'], + docsDeleted: stats['docs.deleted'], + storeSizeBytes: stats['store.size'], + primaryStoreSizeBytes: stats['pri.store.size'], + }; + }); + }) + ); + + return { + indices, + }; + } + + private async getCoreUsageData( + savedObjects: SavedObjectsServiceStart, + elasticsearch: ElasticsearchServiceStart + ): Promise { + if ( + this.elasticsearchConfig == null || + this.httpConfig == null || + this.soConfig == null || + this.opsMetrics == null + ) { + throw new Error('Unable to read config values. Ensure that setup() has completed.'); + } + + const es = this.elasticsearchConfig; + const soUsageData = await this.getSavedObjectIndicesUsageData(savedObjects, elasticsearch); + + const http = this.httpConfig; + return { + config: { + elasticsearch: { + apiVersion: es.apiVersion, + sniffOnStart: es.sniffOnStart, + sniffIntervalMs: es.sniffInterval !== false ? es.sniffInterval.asMilliseconds() : -1, + sniffOnConnectionFault: es.sniffOnConnectionFault, + numberOfHostsConfigured: Array.isArray(es.hosts) + ? es.hosts.length + : isConfigured.string(es.hosts) + ? 1 + : 0, + customHeadersConfigured: isConfigured.record(es.customHeaders), + healthCheckDelayMs: es.healthCheck.delay.asMilliseconds(), + logQueries: es.logQueries, + pingTimeoutMs: es.pingTimeout.asMilliseconds(), + requestHeadersWhitelistConfigured: isConfigured.stringOrArray( + es.requestHeadersWhitelist, + ['authorization'] + ), + requestTimeoutMs: es.requestTimeout.asMilliseconds(), + shardTimeoutMs: es.shardTimeout.asMilliseconds(), + ssl: { + alwaysPresentCertificate: es.ssl.alwaysPresentCertificate, + certificateAuthoritiesConfigured: isConfigured.stringOrArray( + es.ssl.certificateAuthorities + ), + certificateConfigured: isConfigured.string(es.ssl.certificate), + keyConfigured: isConfigured.string(es.ssl.key), + verificationMode: es.ssl.verificationMode, + truststoreConfigured: isConfigured.record(es.ssl.truststore), + keystoreConfigured: isConfigured.record(es.ssl.keystore), + }, + }, + http: { + basePathConfigured: isConfigured.string(http.basePath), + maxPayloadInBytes: http.maxPayload.getValueInBytes(), + rewriteBasePath: http.rewriteBasePath, + keepaliveTimeout: http.keepaliveTimeout, + socketTimeout: http.socketTimeout, + compression: { + enabled: http.compression.enabled, + referrerWhitelistConfigured: isConfigured.array(http.compression.referrerWhitelist), + }, + xsrf: { + disableProtection: http.xsrf.disableProtection, + whitelistConfigured: isConfigured.array(http.xsrf.whitelist), + }, + requestId: { + allowFromAnyIp: http.requestId.allowFromAnyIp, + ipAllowlistConfigured: isConfigured.array(http.requestId.ipAllowlist), + }, + ssl: { + certificateAuthoritiesConfigured: isConfigured.stringOrArray( + http.ssl.certificateAuthorities + ), + certificateConfigured: isConfigured.string(http.ssl.certificate), + cipherSuites: http.ssl.cipherSuites, + keyConfigured: isConfigured.string(http.ssl.key), + redirectHttpFromPortConfigured: isConfigured.number(http.ssl.redirectHttpFromPort), + supportedProtocols: http.ssl.supportedProtocols, + clientAuthentication: http.ssl.clientAuthentication, + keystoreConfigured: isConfigured.record(http.ssl.keystore), + truststoreConfigured: isConfigured.record(http.ssl.truststore), + }, + }, + + logging: { + appendersTypesUsed: Array.from( + Array.from(this.loggingConfig?.appenders.values() ?? []) + .reduce((acc, a) => acc.add(a.kind), new Set()) + .values() + ), + loggersConfiguredCount: this.loggingConfig?.loggers.length ?? 0, + }, + + savedObjects: { + maxImportPayloadBytes: this.soConfig.maxImportPayloadBytes.getValueInBytes(), + maxImportExportSizeBytes: this.soConfig.maxImportExportSize.getValueInBytes(), + }, + }, + environment: { + memory: { + heapSizeLimit: this.opsMetrics.process.memory.heap.size_limit, + heapTotalBytes: this.opsMetrics.process.memory.heap.total_in_bytes, + heapUsedBytes: this.opsMetrics.process.memory.heap.used_in_bytes, + }, + }, + services: { + savedObjects: soUsageData, + }, + }; + } + + setup({ metrics }: SetupDeps) { + metrics + .getOpsMetrics$() + .pipe(takeUntil(this.stop$)) + .subscribe((opsMetrics) => (this.opsMetrics = opsMetrics)); + + this.configService + .atPath('elasticsearch') + .pipe(takeUntil(this.stop$)) + .subscribe((config) => { + this.elasticsearchConfig = config; + }); + + this.configService + .atPath('server') + .pipe(takeUntil(this.stop$)) + .subscribe((config) => { + this.httpConfig = config; + }); + + this.configService + .atPath('logging') + .pipe(takeUntil(this.stop$)) + .subscribe((config) => { + this.loggingConfig = config; + }); + + this.configService + .atPath('savedObjects') + .pipe(takeUntil(this.stop$)) + .subscribe((config) => { + this.soConfig = config; + }); + + this.configService + .atPath('kibana') + .pipe(takeUntil(this.stop$)) + .subscribe((config) => { + this.kibanaConfig = config; + }); + } + + start({ savedObjects, elasticsearch }: StartDeps) { + return { + getCoreUsageData: () => { + return this.getCoreUsageData(savedObjects, elasticsearch); + }, + }; + } + + stop() { + this.stop$.next(); + this.stop$.complete(); + } +} diff --git a/src/core/server/core_usage_data/index.ts b/src/core/server/core_usage_data/index.ts new file mode 100644 index 0000000000000..b78c126657ef6 --- /dev/null +++ b/src/core/server/core_usage_data/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export { CoreUsageDataStart } from './types'; +export { CoreUsageDataService } from './core_usage_data_service'; + +// Because of #79265 we need to explicity import, then export these types for +// scripts/telemetry_check.js to work as expected +import { + CoreUsageData, + CoreConfigUsageData, + CoreEnvironmentUsageData, + CoreServicesUsageData, +} from './types'; + +export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; diff --git a/src/core/server/core_usage_data/is_configured.test.ts b/src/core/server/core_usage_data/is_configured.test.ts new file mode 100644 index 0000000000000..e5d04946b8766 --- /dev/null +++ b/src/core/server/core_usage_data/is_configured.test.ts @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isConfigured } from './is_configured'; + +describe('isConfigured', () => { + describe('#string', () => { + it('returns true for a non-empty string', () => { + expect(isConfigured.string('I am configured')).toEqual(true); + }); + + it('returns false for an empty string', () => { + expect(isConfigured.string(' ')).toEqual(false); + expect(isConfigured.string(' ')).toEqual(false); + }); + + it('returns false for undefined', () => { + expect(isConfigured.string(undefined)).toEqual(false); + }); + + it('returns false for null', () => { + expect(isConfigured.string(null as any)).toEqual(false); + }); + + it('returns false for a record', () => { + expect(isConfigured.string({} as any)).toEqual(false); + expect(isConfigured.string({ key: 'hello' } as any)).toEqual(false); + }); + + it('returns false for an array', () => { + expect(isConfigured.string([] as any)).toEqual(false); + expect(isConfigured.string(['hello'] as any)).toEqual(false); + }); + }); + + describe('array', () => { + it('returns true for a non-empty array', () => { + expect(isConfigured.array(['a'])).toEqual(true); + expect(isConfigured.array([{}])).toEqual(true); + expect(isConfigured.array([{ key: 'hello' }])).toEqual(true); + }); + + it('returns false for an empty array', () => { + expect(isConfigured.array([])).toEqual(false); + }); + + it('returns false for undefined', () => { + expect(isConfigured.array(undefined)).toEqual(false); + }); + + it('returns false for null', () => { + expect(isConfigured.array(null as any)).toEqual(false); + }); + + it('returns false for a string', () => { + expect(isConfigured.array('string')).toEqual(false); + }); + + it('returns false for a record', () => { + expect(isConfigured.array({} as any)).toEqual(false); + }); + }); + + describe('stringOrArray', () => { + const arraySpy = jest.spyOn(isConfigured, 'array'); + const stringSpy = jest.spyOn(isConfigured, 'string'); + + it('calls #array for an array', () => { + isConfigured.stringOrArray([]); + expect(arraySpy).toHaveBeenCalledWith([]); + }); + + it('calls #string for non-array values', () => { + isConfigured.stringOrArray('string'); + expect(stringSpy).toHaveBeenCalledWith('string'); + }); + }); + + describe('record', () => { + it('returns true for a non-empty record', () => { + expect(isConfigured.record({ key: 'hello' })).toEqual(true); + expect(isConfigured.record({ key: undefined })).toEqual(true); + }); + + it('returns false for an empty record', () => { + expect(isConfigured.record({})).toEqual(false); + }); + it('returns false for undefined', () => { + expect(isConfigured.record(undefined)).toEqual(false); + }); + it('returns false for null', () => { + expect(isConfigured.record(null as any)).toEqual(false); + }); + }); + + describe('number', () => { + it('returns true for a valid number', () => { + expect(isConfigured.number(0)).toEqual(true); + expect(isConfigured.number(-0)).toEqual(true); + expect(isConfigured.number(1)).toEqual(true); + expect(isConfigured.number(-0)).toEqual(true); + }); + + it('returns false for NaN', () => { + expect(isConfigured.number(Number.NaN)).toEqual(false); + }); + + it('returns false for a string', () => { + expect(isConfigured.number('1' as any)).toEqual(false); + expect(isConfigured.number('' as any)).toEqual(false); + }); + + it('returns false for undefined', () => { + expect(isConfigured.number(undefined)).toEqual(false); + }); + + it('returns false for null', () => { + expect(isConfigured.number(null as any)).toEqual(false); + }); + }); +}); diff --git a/src/core/server/core_usage_data/is_configured.ts b/src/core/server/core_usage_data/is_configured.ts new file mode 100644 index 0000000000000..e66f990f1037c --- /dev/null +++ b/src/core/server/core_usage_data/is_configured.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { isEqual } from 'lodash'; + +/** + * Test whether a given config value is configured based on it's schema type. + * Our configuration schema and code often accept and ignore empty values like + * `elasticsearch.customHeaders: {}`. However, for telemetry purposes, we're + * only interested when these values have been set to something that will + * change the behaviour of Kibana. + */ +export const isConfigured = { + /** + * config is a string with non-zero length + */ + string: (config?: string): boolean => { + return (config?.trim?.()?.length ?? 0) > 0; + }, + /** + * config is an array with non-zero length + */ + array: (config?: unknown[] | string, defaultValue?: any): boolean => { + return Array.isArray(config) + ? (config?.length ?? 0) > 0 && !isEqual(config, defaultValue) + : false; + }, + /** + * config is a string or array of strings where each element has non-zero length + */ + stringOrArray: (config?: string[] | string, defaultValue?: any): boolean => { + return Array.isArray(config) + ? isConfigured.array(config, defaultValue) + : isConfigured.string(config); + }, + /** + * config is a record with at least one key + */ + record: (config?: Record): boolean => { + return config != null && typeof config === 'object' && Object.keys(config).length > 0; + }, + /** + * config is a number + */ + number: (config?: number): boolean => { + // kbn-config-schema already does NaN validation, but doesn't hurt to be sure + return typeof config === 'number' && !isNaN(config); + }, +}; diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts new file mode 100644 index 0000000000000..52d2eadcf1377 --- /dev/null +++ b/src/core/server/core_usage_data/types.ts @@ -0,0 +1,162 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Type describing Core's usage data payload + * @internal + */ +export interface CoreUsageData { + config: CoreConfigUsageData; + services: CoreServicesUsageData; + environment: CoreEnvironmentUsageData; +} + +/** + * Usage data from Core services + * @internal + */ +export interface CoreServicesUsageData { + savedObjects: { + // scripts/telemetry_check.js does not support parsing Array<{...}> types + // so we have to disable eslint here and use {...}[] + // eslint-disable-next-line @typescript-eslint/array-type + indices: { + alias: string; + docsCount: number; + docsDeleted: number; + storeSizeBytes: number; + primaryStoreSizeBytes: number; + }[]; + }; +} + +/** + * Usage data on this Kibana node's runtime environment. + * @internal + */ +export interface CoreEnvironmentUsageData { + memory: { + heapTotalBytes: number; + heapUsedBytes: number; + /** V8 heap size limit */ + heapSizeLimit: number; + }; +} + +/** + * Usage data on this cluster's configuration of Core features + * @internal + */ +export interface CoreConfigUsageData { + elasticsearch: { + sniffOnStart: boolean; + sniffIntervalMs?: number; + sniffOnConnectionFault: boolean; + numberOfHostsConfigured: number; + requestHeadersWhitelistConfigured: boolean; + customHeadersConfigured: boolean; + shardTimeoutMs: number; + requestTimeoutMs: number; + pingTimeoutMs: number; + logQueries: boolean; + ssl: { + verificationMode: 'none' | 'certificate' | 'full'; + certificateAuthoritiesConfigured: boolean; + certificateConfigured: boolean; + keyConfigured: boolean; + keystoreConfigured: boolean; + truststoreConfigured: boolean; + alwaysPresentCertificate: boolean; + }; + apiVersion: string; + healthCheckDelayMs: number; + }; + + http: { + basePathConfigured: boolean; + maxPayloadInBytes: number; + rewriteBasePath: boolean; + keepaliveTimeout: number; + socketTimeout: number; + compression: { + enabled: boolean; + referrerWhitelistConfigured: boolean; + }; + xsrf: { + disableProtection: boolean; + whitelistConfigured: boolean; + }; + requestId: { + allowFromAnyIp: boolean; + ipAllowlistConfigured: boolean; + }; + ssl: { + certificateAuthoritiesConfigured: boolean; + certificateConfigured: boolean; + cipherSuites: string[]; + keyConfigured: boolean; + keystoreConfigured: boolean; + truststoreConfigured: boolean; + redirectHttpFromPortConfigured: boolean; + supportedProtocols: string[]; + clientAuthentication: 'none' | 'optional' | 'required'; + }; + }; + + logging: { + appendersTypesUsed: string[]; + loggersConfiguredCount: number; + }; + + // plugins: { + // /** list of built-in plugins that are disabled */ + // firstPartyDisabled: string[]; + // /** list of third-party plugins that are installed and enabled */ + // thirdParty: string[]; + // }; + + savedObjects: { + maxImportPayloadBytes: number; + maxImportExportSizeBytes: number; + }; + + // uiSettings: { + // overridesCount: number; + // }; +} + +/** + * Internal API for getting Core's usage data payload. + * + * @note This API should never be used to drive application logic and is only + * intended for telemetry purposes. + * + * @internal + */ +export interface CoreUsageDataStart { + /** + * Internal API for getting Core's usage data payload. + * + * @note This API should never be used to drive application logic and is only + * intended for telemetry purposes. + * + * @internal + * */ + getCoreUsageData(): Promise; +} diff --git a/src/core/server/elasticsearch/client/client_config.test.ts b/src/core/server/elasticsearch/client/client_config.test.ts index 675d8840e7118..e8083836d3c1e 100644 --- a/src/core/server/elasticsearch/client/client_config.test.ts +++ b/src/core/server/elasticsearch/client/client_config.test.ts @@ -19,6 +19,7 @@ import { duration } from 'moment'; import { ElasticsearchClientConfig, parseClientOptions } from './client_config'; +import { DEFAULT_HEADERS } from '../default_headers'; const createConfig = ( parts: Partial = {} @@ -36,6 +37,18 @@ const createConfig = ( }; describe('parseClientOptions', () => { + it('includes headers designing the HTTP request as originating from Kibana by default', () => { + const config = createConfig({}); + + expect(parseClientOptions(config, false)).toEqual( + expect.objectContaining({ + headers: { + ...DEFAULT_HEADERS, + }, + }) + ); + }); + describe('basic options', () => { it('`customHeaders` option', () => { const config = createConfig({ @@ -48,6 +61,7 @@ describe('parseClientOptions', () => { expect(parseClientOptions(config, false)).toEqual( expect.objectContaining({ headers: { + ...DEFAULT_HEADERS, foo: 'bar', hello: 'dolly', }, @@ -55,6 +69,25 @@ describe('parseClientOptions', () => { ); }); + it('`customHeaders` take precedence to default kibana headers', () => { + const customHeader = { + [Object.keys(DEFAULT_HEADERS)[0]]: 'foo', + }; + const config = createConfig({ + customHeaders: { + ...customHeader, + }, + }); + + expect(parseClientOptions(config, false)).toEqual( + expect.objectContaining({ + headers: { + ...customHeader, + }, + }) + ); + }); + it('`keepAlive` option', () => { expect(parseClientOptions(createConfig({ keepAlive: true }), false)).toEqual( expect.objectContaining({ agent: { keepAlive: true } }) diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index f365ca331cfea..f24c0d86550b8 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -22,6 +22,7 @@ import { URL } from 'url'; import { Duration } from 'moment'; import { ClientOptions, NodeOptions } from '@elastic/elasticsearch'; import { ElasticsearchConfig } from '../elasticsearch_config'; +import { DEFAULT_HEADERS } from '../default_headers'; /** * Configuration options to be used to create a {@link IClusterClient | cluster client} using the @@ -61,7 +62,10 @@ export function parseClientOptions( const clientOptions: ClientOptions = { sniffOnStart: config.sniffOnStart, sniffOnConnectionFault: config.sniffOnConnectionFault, - headers: config.customHeaders, + headers: { + ...DEFAULT_HEADERS, + ...config.customHeaders, + }, }; if (config.pingTimeout != null) { diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index e35d9962e9e7e..429fea65704d8 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -24,6 +24,7 @@ import { GetAuthHeaders } from '../../http'; import { elasticsearchClientMock } from './mocks'; import { ClusterClient } from './cluster_client'; import { ElasticsearchClientConfig } from './client_config'; +import { DEFAULT_HEADERS } from '../default_headers'; const createConfig = ( parts: Partial = {} @@ -127,7 +128,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { foo: 'bar', 'x-opaque-id': expect.any(String) }, + headers: { ...DEFAULT_HEADERS, foo: 'bar', 'x-opaque-id': expect.any(String) }, }); }); @@ -147,7 +148,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, }); }); @@ -171,7 +172,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { authorization: 'auth', 'x-opaque-id': expect.any(String) }, + headers: { ...DEFAULT_HEADERS, authorization: 'auth', 'x-opaque-id': expect.any(String) }, }); }); @@ -193,6 +194,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ headers: { + ...DEFAULT_HEADERS, foo: 'bar', hello: 'dolly', 'x-opaque-id': expect.any(String), @@ -214,6 +216,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ headers: { + ...DEFAULT_HEADERS, 'x-opaque-id': 'my-fake-id', }, }); @@ -239,6 +242,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ headers: { + ...DEFAULT_HEADERS, foo: 'auth', hello: 'dolly', 'x-opaque-id': expect.any(String), @@ -266,6 +270,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ headers: { + ...DEFAULT_HEADERS, foo: 'request', hello: 'dolly', 'x-opaque-id': expect.any(String), @@ -273,6 +278,52 @@ describe('ClusterClient', () => { }); }); + it('respect the precedence of config headers over default headers', () => { + const headerKey = Object.keys(DEFAULT_HEADERS)[0]; + const config = createConfig({ + customHeaders: { + [headerKey]: 'foo', + }, + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest(); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + [headerKey]: 'foo', + 'x-opaque-id': expect.any(String), + }, + }); + }); + + it('respect the precedence of request headers over default headers', () => { + const headerKey = Object.keys(DEFAULT_HEADERS)[0]; + const config = createConfig({ + requestHeadersWhitelist: [headerKey], + }); + getAuthHeaders.mockReturnValue({}); + + const clusterClient = new ClusterClient(config, logger, getAuthHeaders); + const request = httpServerMock.createKibanaRequest({ + headers: { [headerKey]: 'foo' }, + }); + + clusterClient.asScoped(request); + + expect(scopedClient.child).toHaveBeenCalledTimes(1); + expect(scopedClient.child).toHaveBeenCalledWith({ + headers: { + [headerKey]: 'foo', + 'x-opaque-id': expect.any(String), + }, + }); + }); + it('respect the precedence of x-opaque-id header over config headers', () => { const config = createConfig({ customHeaders: { @@ -292,6 +343,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ headers: { + ...DEFAULT_HEADERS, 'x-opaque-id': 'from request', }, }); @@ -315,7 +367,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { authorization: 'auth' }, + headers: { ...DEFAULT_HEADERS, authorization: 'auth' }, }); }); @@ -339,7 +391,7 @@ describe('ClusterClient', () => { expect(scopedClient.child).toHaveBeenCalledTimes(1); expect(scopedClient.child).toHaveBeenCalledWith({ - headers: { foo: 'bar' }, + headers: { ...DEFAULT_HEADERS, foo: 'bar' }, }); }); }); diff --git a/src/core/server/elasticsearch/client/cluster_client.ts b/src/core/server/elasticsearch/client/cluster_client.ts index ffe0c10321fff..2294df89d4274 100644 --- a/src/core/server/elasticsearch/client/cluster_client.ts +++ b/src/core/server/elasticsearch/client/cluster_client.ts @@ -26,6 +26,7 @@ import { ElasticsearchClient } from './types'; import { configureClient } from './configure_client'; import { ElasticsearchClientConfig } from './client_config'; import { ScopedClusterClient, IScopedClusterClient } from './scoped_cluster_client'; +import { DEFAULT_HEADERS } from '../default_headers'; const noop = () => undefined; @@ -108,6 +109,7 @@ export class ClusterClient implements ICustomClusterClient { } return { + ...DEFAULT_HEADERS, ...this.config.customHeaders, ...scopedHeaders, }; diff --git a/src/core/server/elasticsearch/default_headers.ts b/src/core/server/elasticsearch/default_headers.ts new file mode 100644 index 0000000000000..4cc4b4b2507c4 --- /dev/null +++ b/src/core/server/elasticsearch/default_headers.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { deepFreeze } from '@kbn/std'; + +export const DEFAULT_HEADERS = deepFreeze({ + // Elasticsearch uses this to identify when a request is coming from Kibana, to allow Kibana to + // access system indices using the standard ES APIs without logging a warning. After migrating to + // use the new system index APIs, this header can be removed. + 'x-elastic-product-origin': 'kibana', +}); diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts index d6b65da7726d2..1e85932caa8ae 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.test.ts @@ -23,6 +23,7 @@ import { LegacyElasticsearchClientConfig, parseElasticsearchClientConfig, } from './elasticsearch_client_config'; +import { DEFAULT_HEADERS } from '../default_headers'; const logger = loggingSystemMock.create(); afterEach(() => jest.clearAllMocks()); @@ -41,26 +42,27 @@ test('parses minimally specified config', () => { logger.get() ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "master", - "hosts": Array [ Object { - "headers": Object { - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": false, - "sniffOnStart": false, -} -`); + "apiVersion": "master", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "localhost", + "path": "/elasticsearch", + "port": "80", + "protocol": "http:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": false, + "sniffOnStart": false, + } + `); }); test('parses fully specified config', () => { @@ -104,63 +106,66 @@ test('parses fully specified config', () => { expect(elasticsearchConfig.ssl).not.toBe(elasticsearchClientConfig.ssl); expect(elasticsearchClientConfig).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "auth": "elastic:changeme", - "headers": Object { - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - Object { - "auth": "elastic:changeme", - "headers": Object { - "xsrf": "something", - }, - "host": "domain.com", - "path": "/elasticsearch", - "port": "1234", - "protocol": "http:", - "query": null, - }, Object { - "auth": "elastic:changeme", - "headers": Object { - "xsrf": "something", + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "auth": "elastic:changeme", + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "localhost", + "path": "/elasticsearch", + "port": "80", + "protocol": "http:", + "query": null, + }, + Object { + "auth": "elastic:changeme", + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "domain.com", + "path": "/elasticsearch", + "port": "1234", + "protocol": "http:", + "query": null, + }, + Object { + "auth": "elastic:changeme", + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "pingTimeout": 12345, + "requestTimeout": 54321, + "sniffInterval": 11223344, + "sniffOnConnectionFault": true, + "sniffOnStart": true, + "ssl": Object { + "ca": Array [ + "content-of-ca-path-1", + "content-of-ca-path-2", + ], + "cert": "content-of-certificate-path", + "checkServerIdentity": [Function], + "key": "content-of-key-path", + "passphrase": "key-pass", + "rejectUnauthorized": true, }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "pingTimeout": 12345, - "requestTimeout": 54321, - "sniffInterval": 11223344, - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": Array [ - "content-of-ca-path-1", - "content-of-ca-path-2", - ], - "cert": "content-of-certificate-path", - "checkServerIdentity": [Function], - "key": "content-of-key-path", - "passphrase": "key-pass", - "rejectUnauthorized": true, - }, -} -`); + } + `); }); test('parses config timeouts of moment.Duration type', () => { @@ -181,29 +186,30 @@ test('parses config timeouts of moment.Duration type', () => { logger.get() ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "master", - "hosts": Array [ Object { - "headers": Object { - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "9200", - "protocol": "http:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "pingTimeout": 100, - "requestTimeout": 30000, - "sniffInterval": 60000, - "sniffOnConnectionFault": false, - "sniffOnStart": false, -} -`); + "apiVersion": "master", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "localhost", + "path": "/elasticsearch", + "port": "9200", + "protocol": "http:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "pingTimeout": 100, + "requestTimeout": 30000, + "sniffInterval": 60000, + "sniffOnConnectionFault": false, + "sniffOnStart": false, + } + `); }); describe('#auth', () => { @@ -225,36 +231,38 @@ describe('#auth', () => { { auth: false } ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "xsrf": "something", - }, - "host": "localhost", - "path": "/elasticsearch", - "port": "80", - "protocol": "http:", - "query": null, - }, - Object { - "headers": Object { - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "localhost", + "path": "/elasticsearch", + "port": "80", + "protocol": "http:", + "query": null, + }, + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); }); test('is not set if username is not specified', () => { @@ -274,26 +282,27 @@ Object { { auth: true } ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "xsrf": "something", - }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); }); test('is not set if password is not specified', () => { @@ -313,26 +322,48 @@ Object { { auth: true } ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object { - "xsrf": "something", + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + "xsrf": "something", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + } + `); + }); +}); + +describe('#customHeaders', () => { + test('override the default headers', () => { + const headerKey = Object.keys(DEFAULT_HEADERS)[0]; + const parsedConfig = parseElasticsearchClientConfig( + { + apiVersion: 'master', + customHeaders: { [headerKey]: 'foo' }, + logQueries: false, + sniffOnStart: false, + sniffOnConnectionFault: false, + hosts: ['http://localhost/elasticsearch'], + requestHeadersWhitelist: [], }, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, -} -`); + logger.get() + ); + expect(parsedConfig.hosts[0].headers).toEqual({ + [headerKey]: 'foo', + }); }); }); @@ -361,24 +392,24 @@ describe('#log', () => { expect(typeof esLogger.close).toBe('function'); expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(` -Object { - "debug": Array [], - "error": Array [ - Array [ - "some-error", - ], - ], - "fatal": Array [], - "info": Array [], - "log": Array [], - "trace": Array [], - "warn": Array [ - Array [ - "some-warning", - ], - ], -} -`); + Object { + "debug": Array [], + "error": Array [ + Array [ + "some-error", + ], + ], + "fatal": Array [], + "info": Array [], + "log": Array [], + "trace": Array [], + "warn": Array [ + Array [ + "some-warning", + ], + ], + } + `); }); test('default logger with #logQueries = true', () => { @@ -407,35 +438,35 @@ Object { expect(typeof esLogger.close).toBe('function'); expect(loggingSystemMock.collect(logger)).toMatchInlineSnapshot(` -Object { - "debug": Array [ - Array [ - "304 -METHOD /some-path -?query=2", Object { - "tags": Array [ - "query", + "debug": Array [ + Array [ + "304 + METHOD /some-path + ?query=2", + Object { + "tags": Array [ + "query", + ], + }, + ], ], - }, - ], - ], - "error": Array [ - Array [ - "some-error", - ], - ], - "fatal": Array [], - "info": Array [], - "log": Array [], - "trace": Array [], - "warn": Array [ - Array [ - "some-warning", - ], - ], -} -`); + "error": Array [ + Array [ + "some-error", + ], + ], + "fatal": Array [], + "info": Array [], + "log": Array [], + "trace": Array [], + "warn": Array [ + Array [ + "some-warning", + ], + ], + } + `); }); test('custom logger', () => { @@ -476,28 +507,30 @@ describe('#ssl', () => { logger.get() ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object {}, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "rejectUnauthorized": false, - }, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + "ssl": Object { + "ca": undefined, + "rejectUnauthorized": false, + }, + } + `); }); test('#verificationMode = certificate', () => { @@ -521,29 +554,31 @@ Object { ).toBeUndefined(); expect(clientConfig).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object {}, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "checkServerIdentity": [Function], - "rejectUnauthorized": true, - }, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + "ssl": Object { + "ca": undefined, + "checkServerIdentity": [Function], + "rejectUnauthorized": true, + }, + } + `); }); test('#verificationMode = full', () => { @@ -562,28 +597,30 @@ Object { logger.get() ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object {}, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": undefined, - "rejectUnauthorized": true, - }, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + "ssl": Object { + "ca": undefined, + "rejectUnauthorized": true, + }, + } + `); }); test('#verificationMode is unknown', () => { @@ -628,30 +665,32 @@ Object { { ignoreCertAndKey: true } ) ).toMatchInlineSnapshot(` -Object { - "apiVersion": "v7.0.0", - "hosts": Array [ - Object { - "headers": Object {}, - "host": "es.local", - "path": "/", - "port": "443", - "protocol": "https:", - "query": null, - }, - ], - "keepAlive": true, - "log": [Function], - "sniffOnConnectionFault": true, - "sniffOnStart": true, - "ssl": Object { - "ca": Array [ - "content-of-ca-path", - ], - "checkServerIdentity": [Function], - "rejectUnauthorized": true, - }, -} -`); + Object { + "apiVersion": "v7.0.0", + "hosts": Array [ + Object { + "headers": Object { + "x-elastic-product-origin": "kibana", + }, + "host": "es.local", + "path": "/", + "port": "443", + "protocol": "https:", + "query": null, + }, + ], + "keepAlive": true, + "log": [Function], + "sniffOnConnectionFault": true, + "sniffOnStart": true, + "ssl": Object { + "ca": Array [ + "content-of-ca-path", + ], + "checkServerIdentity": [Function], + "rejectUnauthorized": true, + }, + } + `); }); }); diff --git a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts index 6896c0a2e301f..35681ac7a247d 100644 --- a/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts +++ b/src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts @@ -25,6 +25,7 @@ import url from 'url'; import { pick } from '@kbn/std'; import { Logger } from '../../logging'; import { ElasticsearchConfig } from '../elasticsearch_config'; +import { DEFAULT_HEADERS } from '../default_headers'; /** * @privateRemarks Config that consumers can pass to the Elasticsearch JS client is complex and includes @@ -130,7 +131,10 @@ export function parseElasticsearchClientConfig( protocol: uri.protocol, path: uri.pathname, query: uri.query, - headers: config.customHeaders, + headers: { + ...DEFAULT_HEADERS, + ...config.customHeaders, + }, }; if (needsAuth) { diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 9deaa73d8aacf..6aad232cf42b6 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -175,6 +175,7 @@ type ToolkitMock = jest.Mocked { return { + render: jest.fn(), next: jest.fn(), rewriteUrl: jest.fn(), }; diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index f81708145edc4..df837dc35505a 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -198,6 +198,7 @@ const createAuthToolkitMock = (): jest.Mocked => ({ }); const createOnPreResponseToolkitMock = (): jest.Mocked => ({ + render: jest.fn(), next: jest.fn(), }); diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 7513e60966085..cb842b2f60268 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -83,6 +83,7 @@ export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth'; export { OnPreResponseHandler, OnPreResponseToolkit, + OnPreResponseRender, OnPreResponseExtensions, OnPreResponseInfo, } from './lifecycle/on_pre_response'; diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index b9548bf7a8d70..59090d101acbc 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -1286,6 +1286,67 @@ describe('OnPreResponse', () => { expect(requestBody).toStrictEqual({}); }); + + it('supports rendering a different response body', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => { + return res.ok({ + headers: { + 'Original-Header-A': 'A', + }, + body: 'original', + }); + }); + + registerOnPreResponse((req, res, t) => { + return t.render({ body: 'overridden' }); + }); + + await server.start(); + + const result = await supertest(innerServer.listener).get('/').expect(200, 'overridden'); + + expect(result.header['original-header-a']).toBe('A'); + }); + + it('supports rendering a different response body + headers', async () => { + const { registerOnPreResponse, server: innerServer, createRouter } = await server.setup( + setupDeps + ); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => { + return res.ok({ + headers: { + 'Original-Header-A': 'A', + 'Original-Header-B': 'B', + }, + body: 'original', + }); + }); + + registerOnPreResponse((req, res, t) => { + return t.render({ + headers: { + 'Original-Header-A': 'AA', + 'New-Header-C': 'C', + }, + body: 'overridden', + }); + }); + + await server.start(); + + const result = await supertest(innerServer.listener).get('/').expect(200, 'overridden'); + + expect(result.header['original-header-a']).toBe('AA'); + expect(result.header['original-header-b']).toBe('B'); + expect(result.header['new-header-c']).toBe('C'); + }); }); describe('run interceptors in the right order', () => { diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts index 4d1b53313a51f..37dddf4dd4767 100644 --- a/src/core/server/http/lifecycle/on_pre_response.ts +++ b/src/core/server/http/lifecycle/on_pre_response.ts @@ -17,16 +17,23 @@ * under the License. */ -import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi'; +import { Lifecycle, Request, ResponseObject, ResponseToolkit as HapiResponseToolkit } from 'hapi'; import Boom from 'boom'; import { Logger } from '../../logging'; import { HapiResponseAdapter, KibanaRequest, ResponseHeaders } from '../router'; enum ResultType { + render = 'render', next = 'next', } +interface Render { + type: ResultType.render; + body: string; + headers?: ResponseHeaders; +} + interface Next { type: ResultType.next; headers?: ResponseHeaders; @@ -35,7 +42,18 @@ interface Next { /** * @internal */ -type OnPreResponseResult = Next; +type OnPreResponseResult = Render | Next; + +/** + * Additional data to extend a response when rendering a new body + * @public + */ +export interface OnPreResponseRender { + /** additional headers to attach to the response */ + headers?: ResponseHeaders; + /** the body to use in the response */ + body: string; +} /** * Additional data to extend a response. @@ -55,6 +73,12 @@ export interface OnPreResponseInfo { } const preResponseResult = { + render(responseRender: OnPreResponseRender): OnPreResponseResult { + return { type: ResultType.render, body: responseRender.body, headers: responseRender?.headers }; + }, + isRender(result: OnPreResponseResult): result is Render { + return result && result.type === ResultType.render; + }, next(responseExtensions?: OnPreResponseExtensions): OnPreResponseResult { return { type: ResultType.next, headers: responseExtensions?.headers }; }, @@ -68,11 +92,14 @@ const preResponseResult = { * @public */ export interface OnPreResponseToolkit { + /** To override the response with a different body */ + render: (responseRender: OnPreResponseRender) => OnPreResponseResult; /** To pass request to the next handler */ next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; } const toolkit: OnPreResponseToolkit = { + render: preResponseResult.render, next: preResponseResult.next, }; @@ -106,26 +133,36 @@ export function adoptToHapiOnPreResponseFormat(fn: OnPreResponseHandler, log: Lo : response.statusCode; const result = await fn(KibanaRequest.from(request), { statusCode }, toolkit); - if (!preResponseResult.isNext(result)) { + + if (preResponseResult.isNext(result)) { + if (result.headers) { + if (isBoom(response)) { + findHeadersIntersection(response.output.headers, result.headers, log); + // hapi wraps all error response in Boom object internally + response.output.headers = { + ...response.output.headers, + ...(result.headers as any), // hapi types don't specify string[] as valid value + }; + } else { + findHeadersIntersection(response.headers, result.headers, log); + setHeaders(response, result.headers); + } + } + } else if (preResponseResult.isRender(result)) { + const overriddenResponse = responseToolkit.response(result.body).code(statusCode); + + const originalHeaders = isBoom(response) ? response.output.headers : response.headers; + setHeaders(overriddenResponse, originalHeaders); + if (result.headers) { + setHeaders(overriddenResponse, result.headers); + } + + return overriddenResponse; + } else { throw new Error( `Unexpected result from OnPreResponse. Expected OnPreResponseResult, but given: ${result}.` ); } - if (result.headers) { - if (isBoom(response)) { - findHeadersIntersection(response.output.headers, result.headers, log); - // hapi wraps all error response in Boom object internally - response.output.headers = { - ...response.output.headers, - ...(result.headers as any), // hapi types don't specify string[] as valid value - }; - } else { - findHeadersIntersection(response.headers, result.headers, log); - for (const [headerName, headerValue] of Object.entries(result.headers)) { - response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value - } - } - } } } catch (error) { log.error(error); @@ -140,6 +177,12 @@ function isBoom(response: any): response is Boom { return response instanceof Boom; } +function setHeaders(response: ResponseObject, headers: ResponseHeaders) { + for (const [headerName, headerValue] of Object.entries(headers)) { + response.header(headerName, headerValue as any); // hapi types don't specify string[] as valid value + } +} + // NOTE: responseHeaders contains not a full list of response headers, but only explicitly set on a response object. // any headers added by hapi internally, like `content-type`, `content-length`, etc. are not present here. function findHeadersIntersection( diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 70ef93963c69f..fc091bd17bdf4 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,6 +64,18 @@ import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; +import { CoreUsageDataStart } from './core_usage_data'; + +// Because of #79265 we need to explicity import, then export these types for +// scripts/telemetry_check.js to work as expected +import { + CoreUsageData, + CoreConfigUsageData, + CoreEnvironmentUsageData, + CoreServicesUsageData, +} from './core_usage_data'; + +export { CoreUsageData, CoreConfigUsageData, CoreEnvironmentUsageData, CoreServicesUsageData }; export { AuditableEvent, Auditor, AuditorFactory, AuditTrailSetup } from './audit_trail'; export { bootstrap } from './bootstrap'; @@ -161,6 +173,7 @@ export { OnPostAuthToolkit, OnPreResponseHandler, OnPreResponseToolkit, + OnPreResponseRender, OnPreResponseExtensions, OnPreResponseInfo, RedirectResponseOptions, @@ -349,6 +362,8 @@ export { StatusServiceSetup, } from './status'; +export { CoreUsageDataStart } from './core_usage_data'; + /** * Plugin specific context passed to a route handler. * @@ -456,6 +471,8 @@ export interface CoreStart { uiSettings: UiSettingsServiceStart; /** {@link AuditTrailSetup} */ auditTrail: AuditTrailStart; + /** @internal {@link CoreUsageDataStart} */ + coreUsageData: CoreUsageDataStart; } export { diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index f5a5edffb0a74..ce58348a14153 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -39,6 +39,7 @@ import { InternalHttpResourcesSetup } from './http_resources'; import { InternalStatusServiceSetup } from './status'; import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; +import { CoreUsageDataStart } from './core_usage_data'; /** @internal */ export interface InternalCoreSetup { @@ -68,6 +69,7 @@ export interface InternalCoreStart { savedObjects: InternalSavedObjectsServiceStart; uiSettings: InternalUiSettingsServiceStart; auditTrail: AuditTrailStart; + coreUsageData: CoreUsageDataStart; } /** diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 086e20c98c1a3..75e8ae6524920 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -217,6 +217,11 @@ export class LegacyService implements CoreService { }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, auditTrail: startDeps.core.auditTrail, + coreUsageData: { + getCoreUsageData: () => { + throw new Error('core.start.coreUsageData.getCoreUsageData is unsupported in legacy'); + }, + }, }; const router = setupDeps.core.http.createRouter('', this.legacyId); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3030cd9f4e0cb..34e85920efb24 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -37,6 +37,7 @@ import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { auditTrailServiceMock } from './audit_trail/audit_trail_service.mock'; +import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -55,6 +56,7 @@ export { renderingMock } from './rendering/rendering_service.mock'; export { statusServiceMock } from './status/status_service.mock'; export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; +export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -157,6 +159,7 @@ function createCoreStartMock() { metrics: metricsServiceMock.createStartContract(), savedObjects: savedObjectsServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), + coreUsageData: coreUsageDataServiceMock.createStartContract(), }; return mock; @@ -190,6 +193,7 @@ function createInternalCoreStartMock() { savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), auditTrail: auditTrailServiceMock.createStartContract(), + coreUsageData: coreUsageDataServiceMock.createStartContract(), }; return startDeps; } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index ab3f471fd7942..a8249ed7e3218 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -251,5 +251,6 @@ export function createPluginStartContext( asScopedToClient: deps.uiSettings.asScopedToClient, }, auditTrail: deps.auditTrail, + coreUsageData: deps.coreUsageData, }; } diff --git a/src/core/server/saved_objects/routes/bulk_create.ts b/src/core/server/saved_objects/routes/bulk_create.ts index 0f925d61ead98..b048c5d8f99bf 100644 --- a/src/core/server/saved_objects/routes/bulk_create.ts +++ b/src/core/server/saved_objects/routes/bulk_create.ts @@ -44,7 +44,7 @@ export const registerBulkCreateRoute = (router: IRouter) => { }) ) ), - namespaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + initialNamespaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), }) ), }, diff --git a/src/core/server/saved_objects/routes/create.ts b/src/core/server/saved_objects/routes/create.ts index 191dbfaa0dbf1..816315705a375 100644 --- a/src/core/server/saved_objects/routes/create.ts +++ b/src/core/server/saved_objects/routes/create.ts @@ -44,16 +44,16 @@ export const registerCreateRoute = (router: IRouter) => { }) ) ), - namespaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + initialNamespaces: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), }), }, }, router.handleLegacyErrors(async (context, req, res) => { const { type, id } = req.params; const { overwrite } = req.query; - const { attributes, migrationVersion, references, namespaces } = req.body; + const { attributes, migrationVersion, references, initialNamespaces } = req.body; - const options = { id, overwrite, migrationVersion, references, namespaces }; + const options = { id, overwrite, migrationVersion, references, initialNamespaces }; const result = await context.core.savedObjects.client.create(type, attributes, options); return res.ok({ body: result }); }) diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index bd76658c21731..c56cdabf6e4cd 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -33,10 +33,11 @@ import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; import { migrationMocks } from './migrations/mocks'; import { ServiceStatusLevels } from '../status'; +import { ISavedObjectTypeRegistry } from './saved_objects_type_registry'; type SavedObjectsServiceContract = PublicMethodsOf; -const createStartContractMock = () => { +const createStartContractMock = (typeRegistry?: jest.Mocked) => { const startContrat: jest.Mocked = { getScopedClient: jest.fn(), createInternalRepository: jest.fn(), @@ -48,13 +49,15 @@ const createStartContractMock = () => { startContrat.getScopedClient.mockReturnValue(savedObjectsClientMock.create()); startContrat.createInternalRepository.mockReturnValue(savedObjectsRepositoryMock.create()); startContrat.createScopedRepository.mockReturnValue(savedObjectsRepositoryMock.create()); - startContrat.getTypeRegistry.mockReturnValue(typeRegistryMock.create()); + startContrat.getTypeRegistry.mockReturnValue(typeRegistry ?? typeRegistryMock.create()); return startContrat; }; -const createInternalStartContractMock = () => { - const internalStartContract: jest.Mocked = createStartContractMock(); +const createInternalStartContractMock = (typeRegistry?: jest.Mocked) => { + const internalStartContract: jest.Mocked = createStartContractMock( + typeRegistry + ); return internalStartContract; }; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 10c7f143e52b9..e93bdb34ecc75 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -635,13 +635,13 @@ describe('SavedObjectsRepository', () => { await test(namespace); }); - it(`adds namespaces instead of namespace`, async () => { + it(`adds initialNamespaces instead of namespace`, async () => { const test = async (namespace) => { const ns2 = 'bar-namespace'; const ns3 = 'baz-namespace'; const objects = [ - { ...obj1, type: MULTI_NAMESPACE_TYPE, namespaces: [ns2] }, - { ...obj2, type: MULTI_NAMESPACE_TYPE, namespaces: [ns3] }, + { ...obj1, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns2] }, + { ...obj2, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [ns3] }, ]; await bulkCreateSuccess(objects, { namespace, overwrite: true }); const body = [ @@ -758,15 +758,15 @@ describe('SavedObjectsRepository', () => { ).rejects.toThrowError(createBadRequestError('"options.namespace" cannot be "*"')); }); - it(`returns error when namespaces is used with a non-multi-namespace object`, async () => { + it(`returns error when initialNamespaces is used with a non-multi-namespace object`, async () => { const test = async (objType) => { - const obj = { ...obj3, type: objType, namespaces: [] }; + const obj = { ...obj3, type: objType, initialNamespaces: [] }; await bulkCreateError( obj, undefined, expectErrorResult( obj, - createBadRequestError('"namespaces" can only be used on multi-namespace types') + createBadRequestError('"initialNamespaces" can only be used on multi-namespace types') ) ); }; @@ -774,14 +774,14 @@ describe('SavedObjectsRepository', () => { await test(NAMESPACE_AGNOSTIC_TYPE); }); - it(`throws when options.namespaces is used with a multi-namespace type and is empty`, async () => { - const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, namespaces: [] }; + it(`throws when options.initialNamespaces is used with a multi-namespace type and is empty`, async () => { + const obj = { ...obj3, type: MULTI_NAMESPACE_TYPE, initialNamespaces: [] }; await bulkCreateError( obj, undefined, expectErrorResult( obj, - createBadRequestError('"namespaces" must be a non-empty array of strings') + createBadRequestError('"initialNamespaces" must be a non-empty array of strings') ) ); }); @@ -1993,13 +1993,13 @@ describe('SavedObjectsRepository', () => { ); }); - it(`adds namespaces instead of namespace`, async () => { - const options = { id, namespace, namespaces: ['bar-namespace', 'baz-namespace'] }; + it(`adds initialNamespaces instead of namespace`, async () => { + const options = { id, namespace, initialNamespaces: ['bar-namespace', 'baz-namespace'] }; await createSuccess(MULTI_NAMESPACE_TYPE, attributes, options); expect(client.create).toHaveBeenCalledWith( expect.objectContaining({ id: `${MULTI_NAMESPACE_TYPE}:${id}`, - body: expect.objectContaining({ namespaces: options.namespaces }), + body: expect.objectContaining({ namespaces: options.initialNamespaces }), }), expect.anything() ); @@ -2021,23 +2021,25 @@ describe('SavedObjectsRepository', () => { }); describe('errors', () => { - it(`throws when options.namespaces is used with a non-multi-namespace object`, async () => { + it(`throws when options.initialNamespaces is used with a non-multi-namespace object`, async () => { const test = async (objType) => { await expect( - savedObjectsRepository.create(objType, attributes, { namespaces: [namespace] }) + savedObjectsRepository.create(objType, attributes, { initialNamespaces: [namespace] }) ).rejects.toThrowError( - createBadRequestError('"options.namespaces" can only be used on multi-namespace types') + createBadRequestError( + '"options.initialNamespaces" can only be used on multi-namespace types' + ) ); }; await test('dashboard'); await test(NAMESPACE_AGNOSTIC_TYPE); }); - it(`throws when options.namespaces is used with a multi-namespace type and is empty`, async () => { + it(`throws when options.initialNamespaces is used with a multi-namespace type and is empty`, async () => { await expect( - savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { namespaces: [] }) + savedObjectsRepository.create(MULTI_NAMESPACE_TYPE, attributes, { initialNamespaces: [] }) ).rejects.toThrowError( - createBadRequestError('"options.namespaces" must be a non-empty array of strings') + createBadRequestError('"options.initialNamespaces" must be a non-empty array of strings') ); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index bae96ceec2783..39aacd6b05b7b 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -230,19 +230,19 @@ export class SavedObjectsRepository { references = [], refresh = DEFAULT_REFRESH_SETTING, originId, - namespaces, + initialNamespaces, version, } = options; const namespace = normalizeNamespace(options.namespace); - if (namespaces) { + if (initialNamespaces) { if (!this._registry.isMultiNamespace(type)) { throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.namespaces" can only be used on multi-namespace types' + '"options.initialNamespaces" can only be used on multi-namespace types' ); - } else if (!namespaces.length) { + } else if (!initialNamespaces.length) { throw SavedObjectsErrorHelpers.createBadRequestError( - '"options.namespaces" must be a non-empty array of strings' + '"options.initialNamespaces" must be a non-empty array of strings' ); } } @@ -262,9 +262,9 @@ export class SavedObjectsRepository { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces // note: this check throws an error if the object is found but does not exist in this namespace const existingNamespaces = await this.preflightGetNamespaces(type, id, namespace); - savedObjectNamespaces = namespaces || existingNamespaces; + savedObjectNamespaces = initialNamespaces || existingNamespaces; } else { - savedObjectNamespaces = namespaces || getSavedObjectNamespaces(namespace); + savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } } @@ -323,14 +323,14 @@ export class SavedObjectsRepository { let error: DecoratedError | undefined; if (!this._allowedTypes.includes(object.type)) { error = SavedObjectsErrorHelpers.createUnsupportedTypeError(object.type); - } else if (object.namespaces) { + } else if (object.initialNamespaces) { if (!this._registry.isMultiNamespace(object.type)) { error = SavedObjectsErrorHelpers.createBadRequestError( - '"namespaces" can only be used on multi-namespace types' + '"initialNamespaces" can only be used on multi-namespace types' ); - } else if (!object.namespaces.length) { + } else if (!object.initialNamespaces.length) { error = SavedObjectsErrorHelpers.createBadRequestError( - '"namespaces" must be a non-empty array of strings' + '"initialNamespaces" must be a non-empty array of strings' ); } } @@ -388,7 +388,7 @@ export class SavedObjectsRepository { let versionProperties; const { esRequestIndex, - object: { namespaces, version, ...object }, + object: { initialNamespaces, version, ...object }, method, } = expectedBulkGetResult.value; if (esRequestIndex !== undefined) { @@ -410,13 +410,13 @@ export class SavedObjectsRepository { }; } savedObjectNamespaces = - namespaces || getSavedObjectNamespaces(namespace, docFound && actualResult); + initialNamespaces || getSavedObjectNamespaces(namespace, docFound && actualResult); versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(object.type)) { savedObjectNamespace = namespace; } else if (this._registry.isMultiNamespace(object.type)) { - savedObjectNamespaces = namespaces || getSavedObjectNamespaces(namespace); + savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } versionProperties = getExpectedVersionProperties(version); } diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index d2b3b89b928c7..6782998d1bf1e 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -56,7 +56,7 @@ export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { * * Note: this can only be used for multi-namespace object types. */ - namespaces?: string[]; + initialNamespaces?: string[]; } /** @@ -79,7 +79,7 @@ export interface SavedObjectsBulkCreateObject { * * Note: this can only be used for multi-namespace object types. */ - namespaces?: string[]; + initialNamespaces?: string[]; } /** diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7742dad150cfa..20bd102e6f507 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -401,9 +401,102 @@ export interface ContextSetup { createContextContainer>(): IContextContainer; } +// @internal +export interface CoreConfigUsageData { + // (undocumented) + elasticsearch: { + sniffOnStart: boolean; + sniffIntervalMs?: number; + sniffOnConnectionFault: boolean; + numberOfHostsConfigured: number; + requestHeadersWhitelistConfigured: boolean; + customHeadersConfigured: boolean; + shardTimeoutMs: number; + requestTimeoutMs: number; + pingTimeoutMs: number; + logQueries: boolean; + ssl: { + verificationMode: 'none' | 'certificate' | 'full'; + certificateAuthoritiesConfigured: boolean; + certificateConfigured: boolean; + keyConfigured: boolean; + keystoreConfigured: boolean; + truststoreConfigured: boolean; + alwaysPresentCertificate: boolean; + }; + apiVersion: string; + healthCheckDelayMs: number; + }; + // (undocumented) + http: { + basePathConfigured: boolean; + maxPayloadInBytes: number; + rewriteBasePath: boolean; + keepaliveTimeout: number; + socketTimeout: number; + compression: { + enabled: boolean; + referrerWhitelistConfigured: boolean; + }; + xsrf: { + disableProtection: boolean; + whitelistConfigured: boolean; + }; + requestId: { + allowFromAnyIp: boolean; + ipAllowlistConfigured: boolean; + }; + ssl: { + certificateAuthoritiesConfigured: boolean; + certificateConfigured: boolean; + cipherSuites: string[]; + keyConfigured: boolean; + keystoreConfigured: boolean; + truststoreConfigured: boolean; + redirectHttpFromPortConfigured: boolean; + supportedProtocols: string[]; + clientAuthentication: 'none' | 'optional' | 'required'; + }; + }; + // (undocumented) + logging: { + appendersTypesUsed: string[]; + loggersConfiguredCount: number; + }; + // (undocumented) + savedObjects: { + maxImportPayloadBytes: number; + maxImportExportSizeBytes: number; + }; +} + +// @internal +export interface CoreEnvironmentUsageData { + // (undocumented) + memory: { + heapTotalBytes: number; + heapUsedBytes: number; + heapSizeLimit: number; + }; +} + // @internal (undocumented) export type CoreId = symbol; +// @internal +export interface CoreServicesUsageData { + // (undocumented) + savedObjects: { + indices: { + alias: string; + docsCount: number; + docsDeleted: number; + storeSizeBytes: number; + primaryStoreSizeBytes: number; + }[]; + }; +} + // @public export interface CoreSetup { // (undocumented) @@ -438,6 +531,8 @@ export interface CoreStart { auditTrail: AuditTrailStart; // (undocumented) capabilities: CapabilitiesStart; + // @internal (undocumented) + coreUsageData: CoreUsageDataStart; // (undocumented) elasticsearch: ElasticsearchServiceStart; // (undocumented) @@ -458,6 +553,21 @@ export interface CoreStatus { savedObjects: ServiceStatus; } +// @internal +export interface CoreUsageData { + // (undocumented) + config: CoreConfigUsageData; + // (undocumented) + environment: CoreEnvironmentUsageData; + // (undocumented) + services: CoreServicesUsageData; +} + +// @internal +export interface CoreUsageDataStart { + getCoreUsageData(): Promise; +} + // @public (undocumented) export interface CountResponse { // (undocumented) @@ -1420,9 +1530,16 @@ export interface OnPreResponseInfo { statusCode: number; } +// @public +export interface OnPreResponseRender { + body: string; + headers?: ResponseHeaders; +} + // @public export interface OnPreResponseToolkit { next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; + render: (responseRender: OnPreResponseRender) => OnPreResponseResult; } // Warning: (ae-forgotten-export) The symbol "OnPreRoutingResult" needs to be exported by the entry point index.d.ts @@ -1819,8 +1936,8 @@ export interface SavedObjectsBulkCreateObject { attributes: T; // (undocumented) id?: string; + initialNamespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; - namespaces?: string[]; originId?: string; // (undocumented) references?: SavedObjectReference[]; @@ -1977,8 +2094,8 @@ export interface SavedObjectsCoreFieldMapping { // @public (undocumented) export interface SavedObjectsCreateOptions extends SavedObjectsBaseOptions { id?: string; + initialNamespaces?: string[]; migrationVersion?: SavedObjectsMigrationVersion; - namespaces?: string[]; originId?: string; overwrite?: boolean; // (undocumented) diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 600f45e0b50da..f38cac4f43768 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -48,6 +48,7 @@ import { config as statusConfig } from './status'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; +import { CoreUsageDataService } from './core_usage_data'; import { CoreRouteHandlerContext } from './core_route_handler_context'; const coreId = Symbol('core'); @@ -72,6 +73,7 @@ export class Server { private readonly logging: LoggingService; private readonly coreApp: CoreApp; private readonly auditTrail: AuditTrailService; + private readonly coreUsageData: CoreUsageDataService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -103,6 +105,7 @@ export class Server { this.httpResources = new HttpResourcesService(core); this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); + this.coreUsageData = new CoreUsageDataService(core); } public async setup() { @@ -184,6 +187,8 @@ export class Server { loggingSystem: this.loggingSystem, }); + this.coreUsageData.setup({ metrics: metricsSetup }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -235,6 +240,10 @@ export class Server { const uiSettingsStart = await this.uiSettings.start(); const metricsStart = await this.metrics.start(); const httpStart = this.http.getStartContract(); + const coreUsageDataStart = this.coreUsageData.start({ + elasticsearch: elasticsearchStart, + savedObjects: savedObjectsStart, + }); this.coreStart = { capabilities: capabilitiesStart, @@ -244,6 +253,7 @@ export class Server { savedObjects: savedObjectsStart, uiSettings: uiSettingsStart, auditTrail: auditTrailStart, + coreUsageData: coreUsageDataStart, }; const pluginsStart = await this.plugins.start(this.coreStart); diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore b/src/dev/build/tasks/bin/scripts/kibana-keystore index d811e70095548..9d2fd64c1c4eb 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-keystore +++ b/src/dev/build/tasks/bin/scripts/kibana-keystore @@ -26,4 +26,4 @@ if [ -f "${CONFIG_DIR}/node.options" ]; then KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" fi -NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_keystore" "$@" +NODE_OPTIONS="$KBN_NODE_OPTS $NODE_OPTIONS" "${NODE}" "${DIR}/src/cli_keystore/dist" "$@" diff --git a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat index 7e227141c8ba3..2214769efc410 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-keystore.bat +++ b/src/dev/build/tasks/bin/scripts/kibana-keystore.bat @@ -28,7 +28,7 @@ IF EXIST "%CONFIG_DIR%\node.options" ( ) TITLE Kibana Keystore -"%NODE%" "%DIR%\src\cli_keystore" %* +"%NODE%" "%DIR%\src\cli_keystore\dist" %* :finally diff --git a/src/dev/build/tasks/bin/scripts/kibana-plugin b/src/dev/build/tasks/bin/scripts/kibana-plugin index f4486e9cf85fb..78fdb7702643f 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-plugin +++ b/src/dev/build/tasks/bin/scripts/kibana-plugin @@ -26,4 +26,4 @@ if [ -f "${CONFIG_DIR}/node.options" ]; then KBN_NODE_OPTS="$(grep -v ^# < ${CONFIG_DIR}/node.options | xargs)" fi -NODE_OPTIONS="--no-warnings $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli_plugin" "$@" +NODE_OPTIONS="--no-warnings $KBN_NODE_OPTS $NODE_OPTIONS" NODE_ENV=production exec "${NODE}" "${DIR}/src/cli_plugin/dist" "$@" diff --git a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat index 4fb30977fda06..0a6d135565e50 100755 --- a/src/dev/build/tasks/bin/scripts/kibana-plugin.bat +++ b/src/dev/build/tasks/bin/scripts/kibana-plugin.bat @@ -32,7 +32,7 @@ IF EXIST "%CONFIG_DIR%\node.options" ( set "NODE_OPTIONS=--no-warnings %NODE_OPTIONS%" TITLE Kibana Server -"%NODE%" "%DIR%\src\cli_plugin" %* +"%NODE%" "%DIR%\src\cli_plugin\dist" %* :finally diff --git a/src/dev/build/tasks/bin/scripts/kibana.bat b/src/dev/build/tasks/bin/scripts/kibana.bat index 98dd9ec05a48c..19bf8157ed7c8 100755 --- a/src/dev/build/tasks/bin/scripts/kibana.bat +++ b/src/dev/build/tasks/bin/scripts/kibana.bat @@ -34,7 +34,7 @@ set "NODE_OPTIONS=--no-warnings --max-http-header-size=65536 %NODE_OPTIONS%" :: This should run independently as the last instruction :: as we need NODE_OPTIONS previously set to expand -"%NODE%" "%DIR%\src\cli" %* +"%NODE%" "%DIR%\src\cli\dist" %* :finally diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index a5039717760ae..b0ace3c63d82e 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -38,9 +38,9 @@ export const CopySource: Task = { '!src/cli/dev.js', '!src/functional_test_runner/**', '!src/dev/**', - '!src/setup_node_env/babel_register/index.js', - '!src/setup_node_env/babel_register/register.js', - '!**/public/**', + // this is the dev-only entry + '!src/setup_node_env/index.js', + '!**/public/**/*.{js,ts,tsx,json}', 'typings/**', 'config/kibana.yml', 'config/node.options', diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 959e1f8dc3e72..0039debe383bd 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -93,6 +93,7 @@ kibana_vars=( path.data pid.file regionmap + security.showInsecureClusterWarning server.basePath server.customResponseHeaders server.compression.enabled diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index 24649a52b729b..e7523c1bf6032 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -58,6 +58,11 @@ RUN curl -L -o /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releas RUN echo "37f2c1f0372a45554f1b89924fbb134fc24c3756efaedf11e07f599494e0eff9 /usr/local/bin/dumb-init" | sha256sum -c - RUN chmod +x /usr/local/bin/dumb-init +RUN mkdir /usr/share/fonts/local +RUN curl -L -o /usr/share/fonts/local/NotoSansCJK-Regular.ttc https://github.com/googlefonts/noto-cjk/raw/NotoSansV2.001/NotoSansCJK-Regular.ttc +RUN echo "5dcd1c336cc9344cb77c03a0cd8982ca8a7dc97d620fd6c9c434e02dcb1ceeb3 /usr/share/fonts/local/NotoSansCJK-Regular.ttc" | sha256sum -c - +RUN fc-cache -v + # Bring in Kibana from the initial stage. COPY --from=builder --chown=1000:0 /usr/share/kibana /usr/share/kibana WORKDIR /usr/share/kibana diff --git a/src/dev/typescript/build_refs.ts b/src/dev/typescript/build_refs.ts index de006bd674e87..fdc1dfbfffa0b 100644 --- a/src/dev/typescript/build_refs.ts +++ b/src/dev/typescript/build_refs.ts @@ -29,7 +29,7 @@ export async function buildAllRefs(log: ToolingLog) { async function buildRefs(log: ToolingLog, projectPath: string) { try { log.debug(`Building TypeScript projects refs for ${projectPath}...`); - await execa(require.resolve('typescript/bin/tsc'), ['-b', projectPath]); + await execa(require.resolve('typescript/bin/tsc'), ['-b', projectPath, '--pretty']); } catch (e) { log.error(e); process.exit(1); @@ -38,17 +38,11 @@ async function buildRefs(log: ToolingLog, projectPath: string) { export async function runBuildRefs() { run( - async ({ log, flags }) => { - await buildRefs(log, flags.project as string); + async ({ log }) => { + await buildAllRefs(log); }, { description: 'Build TypeScript projects', - flags: { - string: ['project'], - help: ` ---project Required, path to the tsconfig.refs.file - `, - }, } ); } diff --git a/src/fixtures/telemetry_collectors/constants.ts b/src/fixtures/telemetry_collectors/constants.ts index 8896c294676c4..5b66161d07101 100644 --- a/src/fixtures/telemetry_collectors/constants.ts +++ b/src/fixtures/telemetry_collectors/constants.ts @@ -58,6 +58,10 @@ export type TypeAliasWithRecord = Usage & Record; export type MappedTypeProps = 'prop1' | 'prop2'; +export type MappedTypeExtraProps = 'prop3' | 'prop4'; + +export type MappedTypeAllProps = MappedTypeProps | MappedTypeExtraProps; + export interface MappedTypes { mappedTypeWithExternallyDefinedProps: { [key in MappedTypeProps]: number; @@ -68,5 +72,6 @@ export interface MappedTypes { } export type RecordWithKnownProps = Record; +export type RecordWithKnownAllProps = Record; export type IndexedAccessType = Pick; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index feeb8e0bf6e4c..e1f03b8a08847 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -25,6 +25,10 @@ const HANDLED_IN_NEW_PLATFORM = Joi.any().description( ); export default () => Joi.object({ + elastic: Joi.object({ + apm: HANDLED_IN_NEW_PLATFORM, + }).default(), + pkg: Joi.object({ version: Joi.string().default(Joi.ref('$version')), branch: Joi.string().default(Joi.ref('$branch')), diff --git a/src/legacy/ui/apm/index.js b/src/legacy/ui/apm/index.js index c43b7b01d1159..e58f6fb73320d 100644 --- a/src/legacy/ui/apm/index.js +++ b/src/legacy/ui/apm/index.js @@ -17,18 +17,10 @@ * under the License. */ -import { getConfig, isKibanaDistributable } from '../../../apm'; +import { getConfig } from '../../../apm'; import agent from 'elastic-apm-node'; -const apmEnabled = !isKibanaDistributable && process.env.ELASTIC_APM_ACTIVE === 'true'; - -export function apmImport() { - return apmEnabled ? 'import { init } from "@elastic/apm-rum"' : ''; -} - -export function apmInit(config) { - return apmEnabled ? `init(${config})` : ''; -} +const apmEnabled = getConfig()?.active; export function getApmConfig(requestPath) { if (!apmEnabled) { @@ -36,11 +28,9 @@ export function getApmConfig(requestPath) { } const config = { ...getConfig('kibana-frontend'), - ...{ - active: true, - pageLoadTransactionName: requestPath, - }, + pageLoadTransactionName: requestPath, }; + /** * Get current active backend transaction to make distrubuted tracing * work for rendering the app diff --git a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx index 8c9e3847844d9..afdd90959eabd 100644 --- a/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/advanced_settings.tsx @@ -121,7 +121,18 @@ export class AdvancedSettingsComponent extends Component< setTimeout(() => { const id = hash.replace('#', ''); const element = document.getElementById(id); - const globalNavOffset = document.getElementById('globalHeaderBars')?.offsetHeight || 0; + + let globalNavOffset = 0; + + const globalNavBars = document + .getElementById('globalHeaderBars') + ?.getElementsByClassName('euiHeader'); + + if (globalNavBars) { + Array.from(globalNavBars).forEach((navBar) => { + globalNavOffset += (navBar as HTMLDivElement).offsetHeight; + }); + } if (element) { element.scrollIntoView(); diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 8ab2a458d78e0..2ae7c6550b0cc 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -81,7 +81,7 @@ import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; -import { IKbnUrlStateStorage, unhashUrl } from '../../../kibana_utils/public'; +import { IKbnUrlStateStorage, setStateToKbnUrl, unhashUrl } from '../../../kibana_utils/public'; import { addFatalError, AngularHttpError, @@ -1079,7 +1079,12 @@ export class DashboardAppController { allowEmbed: true, allowShortUrl: !dashboardConfig.getHideWriteControls() || dashboardCapabilities.createShortUrl, - shareableUrl: unhashUrl(window.location.href), + shareableUrl: setStateToKbnUrl( + '_a', + dashboardStateManager.getAppState(), + { useHash: false, storeInHashQuery: true }, + unhashUrl(window.location.href) + ), objectId: dash.id, objectType: 'dashboard', sharingData: { diff --git a/src/plugins/dashboard/public/application/index.ts b/src/plugins/dashboard/public/application/index.ts index fcd92da33aa5f..2558c49648b10 100644 --- a/src/plugins/dashboard/public/application/index.ts +++ b/src/plugins/dashboard/public/application/index.ts @@ -19,4 +19,4 @@ export * from './embeddable'; export * from './actions'; -export { RenderDeps } from './application'; +export type { RenderDeps } from './application'; diff --git a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts index 77c4a2235d471..5713996ca9f78 100644 --- a/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts +++ b/src/plugins/dashboard/public/application/top_nav/get_top_nav_config.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { AppMountParameters } from 'kibana/public'; import { ViewMode } from '../../embeddable_plugin'; import { TopNavIds } from './top_nav_ids'; import { NavAction } from '../../types'; @@ -31,7 +32,8 @@ import { NavAction } from '../../types'; export function getTopNavConfig( dashboardMode: ViewMode, actions: { [key: string]: NavAction }, - hideWriteControls: boolean + hideWriteControls: boolean, + onAppLeave?: AppMountParameters['onAppLeave'] ) { switch (dashboardMode) { case ViewMode.VIEW: diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx index 321a53361fc7a..09d6f5b4f1e0d 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.mock.tsx @@ -31,19 +31,16 @@ export const mockAttributeService = < R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput >( type: string, - options?: AttributeServiceOptions, + options: AttributeServiceOptions, customCore?: jest.Mocked ): AttributeService => { const core = customCore ? customCore : coreMock.createStart(); - const service = new AttributeService( + return new AttributeService( type, jest.fn(), - core.savedObjects.client, - core.overlays, core.i18n.Context, core.notifications.toasts, - jest.fn().mockReturnValue(() => ({ getDisplayName: () => type })), - options + options, + jest.fn().mockReturnValue(() => ({ getDisplayName: () => type })) ); - return service; }; diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts index ae8f034aec687..d7368b299c411 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.test.ts @@ -20,6 +20,7 @@ import { ATTRIBUTE_SERVICE_KEY } from './attribute_service'; import { mockAttributeService } from './attribute_service.mock'; import { coreMock } from '../../../../core/public/mocks'; +import { OnSaveProps } from '../../../saved_objects/public/save_modal'; interface TestAttributes { title: string; @@ -37,6 +38,30 @@ describe('attributeService', () => { let attributes: TestAttributes; let byValueInput: TestByValueInput; let byReferenceInput: { id: string; savedObjectId: string }; + const defaultSaveMethod = ( + type: string, + testAttributes: TestAttributes, + savedObjectId?: string + ): Promise<{ id: string }> => { + return new Promise(() => { + return { id: '123' }; + }); + }; + const defaultUnwrapMethod = (savedObjectId: string): Promise => { + return new Promise(() => { + return { ...attributes }; + }); + }; + const defaultCheckForDuplicateTitle = (props: OnSaveProps): Promise => { + return new Promise(() => { + return true; + }); + }; + const options = { + saveMethod: defaultSaveMethod, + unwrapMethod: defaultUnwrapMethod, + checkForDuplicateTitle: defaultCheckForDuplicateTitle, + }; beforeEach(() => { attributes = { @@ -55,9 +80,10 @@ describe('attributeService', () => { }); describe('determining input type', () => { - const defaultAttributeService = mockAttributeService(defaultTestType); + const defaultAttributeService = mockAttributeService(defaultTestType, options); const customAttributeService = mockAttributeService( - defaultTestType + defaultTestType, + options ); it('can determine input type given default types', () => { @@ -85,39 +111,32 @@ describe('attributeService', () => { }); describe('unwrapping attributes', () => { - it('can unwrap all default attributes when given reference type input', async () => { - const core = coreMock.createStart(); - core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({ - attributes, + it('does not throw error when given reference type input with no unwrap method', async () => { + const attributeService = mockAttributeService(defaultTestType, { + saveMethod: defaultSaveMethod, + checkForDuplicateTitle: jest.fn(), }); - const attributeService = mockAttributeService( - defaultTestType, - undefined, - core - ); - expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual(attributes); + expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual(byReferenceInput); }); it('returns attributes when when given value type input', async () => { - const attributeService = mockAttributeService(defaultTestType); + const attributeService = mockAttributeService(defaultTestType, options); expect(await attributeService.unwrapAttributes(byValueInput)).toEqual(attributes); }); it('runs attributes through a custom unwrap method', async () => { - const core = coreMock.createStart(); - core.savedObjects.client.get = jest.fn().mockResolvedValueOnce({ - attributes, - }); - const attributeService = mockAttributeService( - defaultTestType, - { - customUnwrapMethod: (savedObject) => ({ - ...savedObject.attributes, - testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' }, - }), + const attributeService = mockAttributeService(defaultTestType, { + saveMethod: defaultSaveMethod, + unwrapMethod: (savedObjectId) => { + return new Promise((resolve) => { + return resolve({ + ...attributes, + testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' }, + }); + }); }, - core - ); + checkForDuplicateTitle: jest.fn(), + }); expect(await attributeService.unwrapAttributes(byReferenceInput)).toEqual({ ...attributes, testAttr2: { array: [1, 2, 3, 4, 5], testAttr3: 'kibanana' }, @@ -127,52 +146,40 @@ describe('attributeService', () => { describe('wrapping attributes', () => { it('returns given attributes when use ref type is false', async () => { - const attributeService = mockAttributeService(defaultTestType); + const attributeService = mockAttributeService(defaultTestType, options); expect(await attributeService.wrapAttributes(attributes, false)).toEqual({ attributes }); }); - it('updates existing saved object with new attributes when given id', async () => { + it('calls saveMethod with appropriate parameters', async () => { const core = coreMock.createStart(); + const saveMethod = jest.fn(); + saveMethod.mockReturnValueOnce({}); const attributeService = mockAttributeService( defaultTestType, - undefined, + { + saveMethod, + unwrapMethod: defaultUnwrapMethod, + checkForDuplicateTitle: defaultCheckForDuplicateTitle, + }, core ); expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual( byReferenceInput ); - expect(core.savedObjects.client.update).toHaveBeenCalledWith( - defaultTestType, - '123', - attributes - ); - }); - - it('creates new saved object with attributes when given no id', async () => { - const core = coreMock.createStart(); - core.savedObjects.client.create = jest.fn().mockResolvedValueOnce({ - id: '678', - }); - const attributeService = mockAttributeService( - defaultTestType, - undefined, - core - ); - expect(await attributeService.wrapAttributes(attributes, true)).toEqual({ - savedObjectId: '678', - }); - expect(core.savedObjects.client.create).toHaveBeenCalledWith(defaultTestType, attributes); + expect(saveMethod).toHaveBeenCalledWith(defaultTestType, attributes, '123'); }); it('uses custom save method when given an id', async () => { - const customSaveMethod = jest.fn().mockReturnValue({ id: '123' }); + const saveMethod = jest.fn().mockReturnValue({ id: '123' }); const attributeService = mockAttributeService(defaultTestType, { - customSaveMethod, + saveMethod, + unwrapMethod: defaultUnwrapMethod, + checkForDuplicateTitle: defaultCheckForDuplicateTitle, }); expect(await attributeService.wrapAttributes(attributes, true, byReferenceInput)).toEqual( byReferenceInput ); - expect(customSaveMethod).toHaveBeenCalledWith( + expect(saveMethod).toHaveBeenCalledWith( defaultTestType, attributes, byReferenceInput.savedObjectId @@ -180,14 +187,16 @@ describe('attributeService', () => { }); it('uses custom save method given no id', async () => { - const customSaveMethod = jest.fn().mockReturnValue({ id: '678' }); + const saveMethod = jest.fn().mockReturnValue({ id: '678' }); const attributeService = mockAttributeService(defaultTestType, { - customSaveMethod, + saveMethod, + unwrapMethod: defaultUnwrapMethod, + checkForDuplicateTitle: defaultCheckForDuplicateTitle, }); expect(await attributeService.wrapAttributes(attributes, true)).toEqual({ savedObjectId: '678', }); - expect(customSaveMethod).toHaveBeenCalledWith(defaultTestType, attributes, undefined); + expect(saveMethod).toHaveBeenCalledWith(defaultTestType, attributes, undefined); }); }); }); diff --git a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx index 7499a6fced72a..b46226ec4ab02 100644 --- a/src/plugins/dashboard/public/attribute_service/attribute_service.tsx +++ b/src/plugins/dashboard/public/attribute_service/attribute_service.tsx @@ -27,22 +27,10 @@ import { IEmbeddable, Container, EmbeddableStart, - EmbeddableFactory, EmbeddableFactoryNotFoundError, } from '../embeddable_plugin'; -import { - SavedObjectsClientContract, - SimpleSavedObject, - I18nStart, - NotificationsStart, - OverlayStart, -} from '../../../../core/public'; -import { - SavedObjectSaveModal, - OnSaveProps, - SaveResult, - checkForDuplicateTitle, -} from '../../../saved_objects/public'; +import { I18nStart, NotificationsStart } from '../../../../core/public'; +import { SavedObjectSaveModal, OnSaveProps, SaveResult } from '../../../saved_objects/public'; /** * The attribute service is a shared, generic service that embeddables can use to provide the functionality @@ -53,12 +41,13 @@ import { export const ATTRIBUTE_SERVICE_KEY = 'attributes'; export interface AttributeServiceOptions { - customSaveMethod?: ( + saveMethod: ( type: string, attributes: A, savedObjectId?: string ) => Promise<{ id?: string } | { error: Error }>; - customUnwrapMethod?: (savedObject: SimpleSavedObject) => A; + checkForDuplicateTitle: (props: OnSaveProps) => Promise; + unwrapMethod?: (savedObjectId: string) => Promise; } export class AttributeService< @@ -68,38 +57,37 @@ export class AttributeService< } = EmbeddableInput & { [ATTRIBUTE_SERVICE_KEY]: SavedObjectAttributes }, RefType extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > { - private embeddableFactory?: EmbeddableFactory; - constructor( private type: string, private showSaveModal: ( saveModal: React.ReactElement, I18nContext: I18nStart['Context'] ) => void, - private savedObjectsClient: SavedObjectsClientContract, - private overlays: OverlayStart, private i18nContext: I18nStart['Context'], private toasts: NotificationsStart['toasts'], - getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'], - private options?: AttributeServiceOptions + private options: AttributeServiceOptions, + getEmbeddableFactory?: EmbeddableStart['getEmbeddableFactory'] ) { if (getEmbeddableFactory) { const factory = getEmbeddableFactory(this.type); if (!factory) { throw new EmbeddableFactoryNotFoundError(this.type); } - this.embeddableFactory = factory; } } + private async defaultUnwrapMethod(input: RefType): Promise { + return new Promise((resolve) => { + // @ts-ignore + return resolve({ ...input }); + }); + } + public async unwrapAttributes(input: RefType | ValType): Promise { if (this.inputIsRefType(input)) { - const savedObject: SimpleSavedObject = await this.savedObjectsClient.get< - SavedObjectAttributes - >(this.type, input.savedObjectId); - return this.options?.customUnwrapMethod - ? this.options?.customUnwrapMethod(savedObject) - : { ...savedObject.attributes }; + return this.options.unwrapMethod + ? await this.options.unwrapMethod(input.savedObjectId) + : await this.defaultUnwrapMethod(input); } return input[ATTRIBUTE_SERVICE_KEY]; } @@ -118,25 +106,11 @@ export class AttributeService< return { [ATTRIBUTE_SERVICE_KEY]: newAttributes } as ValType; } try { - if (this.options?.customSaveMethod) { - const savedItem = await this.options.customSaveMethod( - this.type, - newAttributes, - savedObjectId - ); - if ('id' in savedItem) { - return { ...originalInput, savedObjectId: savedItem.id } as RefType; - } - return { ...originalInput } as RefType; - } - - if (savedObjectId) { - await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes); - return { ...originalInput, savedObjectId } as RefType; + const savedItem = await this.options.saveMethod(this.type, newAttributes, savedObjectId); + if ('id' in savedItem) { + return { ...originalInput, savedObjectId: savedItem.id } as RefType; } - - const savedItem = await this.savedObjectsClient.create(this.type, newAttributes); - return { ...originalInput, savedObjectId: savedItem.id } as RefType; + return { ...originalInput } as RefType; } catch (error) { this.toasts.addDanger({ title: i18n.translate('dashboard.attributeService.saveToLibraryError', { @@ -181,21 +155,7 @@ export class AttributeService< } return new Promise((resolve, reject) => { const onSave = async (props: OnSaveProps): Promise => { - await checkForDuplicateTitle( - { - title: props.newTitle, - copyOnSave: false, - lastSavedTitle: '', - getEsType: () => this.type, - getDisplayName: this.embeddableFactory?.getDisplayName || (() => this.type), - }, - props.isTitleDuplicateConfirmed, - props.onTitleDuplicate, - { - savedObjectsClient: this.savedObjectsClient, - overlays: this.overlays, - } - ); + await this.options.checkForDuplicateTitle(props); try { const newAttributes = { ...input[ATTRIBUTE_SERVICE_KEY] }; newAttributes.title = props.newTitle; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 315afd61c7c44..bf9a3b2b8a217 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -31,7 +31,12 @@ export { } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; -export { DashboardStart, DashboardUrlGenerator, DashboardFeatureFlagConfig } from './plugin'; +export { + DashboardSetup, + DashboardStart, + DashboardUrlGenerator, + DashboardFeatureFlagConfig, +} from './plugin'; export { DASHBOARD_APP_URL_GENERATOR, createDashboardUrlGenerator, diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index eadb3cd207e4d..3325d193e56ed 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -145,7 +145,7 @@ interface StartDependencies { savedObjects: SavedObjectsStart; } -export type Setup = void; +export type DashboardSetup = void; export interface DashboardStart { getSavedDashboardLoader: () => SavedObjectLoader; @@ -164,7 +164,7 @@ export interface DashboardStart { R extends SavedObjectEmbeddableInput = SavedObjectEmbeddableInput >( type: string, - options?: AttributeServiceOptions + options: AttributeServiceOptions ) => AttributeService; } @@ -180,7 +180,7 @@ declare module '../../../plugins/ui_actions/public' { } export class DashboardPlugin - implements Plugin { + implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} private appStateUpdater = new BehaviorSubject(() => ({})); @@ -193,17 +193,8 @@ export class DashboardPlugin public setup( core: CoreSetup, - { - share, - uiActions, - embeddable, - home, - kibanaLegacy, - urlForwarding, - data, - usageCollection, - }: SetupDependencies - ): Setup { + { share, uiActions, embeddable, home, urlForwarding, data, usageCollection }: SetupDependencies + ): DashboardSetup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get< DashboardFeatureFlagConfig >(); @@ -404,6 +395,9 @@ export class DashboardPlugin title: i18n.translate('dashboard.featureCatalogue.dashboardTitle', { defaultMessage: 'Dashboard', }), + subtitle: i18n.translate('dashboard.featureCatalogue.dashboardSubtitle', { + defaultMessage: 'Analyze data in dashboards.', + }), description: i18n.translate('dashboard.featureCatalogue.dashboardDescription', { defaultMessage: 'Display and share a collection of visualizations and saved searches.', }), @@ -411,6 +405,8 @@ export class DashboardPlugin path: `/app/dashboards#${DashboardConstants.LANDING_PAGE_PATH}`, showOnHomePage: false, category: FeatureCatalogueCategory.DATA, + solutionId: 'kibana', + order: 100, }); } } @@ -491,12 +487,10 @@ export class DashboardPlugin new AttributeService( type, showSaveModal, - core.savedObjects.client, - core.overlays, core.i18n.Context, core.notifications.toasts, - embeddable.getEmbeddableFactory, - options + options, + embeddable.getEmbeddableFactory ), }; } diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts index 850b2470dd475..a85f67f5ba56a 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -46,10 +46,10 @@ export const dashboardSavedObjectType: SavedObjectsType = { description: { type: 'text' }, hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { - properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + properties: { searchSourceJSON: { type: 'text', index: false } }, }, - optionsJSON: { type: 'text', index: false, doc_values: false }, - panelsJSON: { type: 'text', index: false, doc_values: false }, + optionsJSON: { type: 'text', index: false }, + panelsJSON: { type: 'text', index: false }, refreshInterval: { properties: { display: { type: 'keyword', index: false, doc_values: false }, diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 3eafea06f9f5b..1a6b770cf2ca8 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -76,6 +76,7 @@ import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/common'; import { IIndexPattern } from '../../index_patterns'; import { IEsSearchRequest, IEsSearchResponse, ISearchOptions } from '../..'; +import { IKibanaSearchRequest, IKibanaSearchResponse } from '../types'; import { ISearchSource, SearchSourceOptions, SearchSourceFields } from './types'; import { FetchHandlers, RequestFailure, getSearchParamsFromRequest, SearchRequest } from './fetch'; @@ -101,7 +102,15 @@ export const searchSourceRequiredUiSettings = [ ]; export interface SearchSourceDependencies extends FetchHandlers { - search: (request: IEsSearchRequest, options: ISearchOptions) => Promise; + // Types are nearly identical to ISearchGeneric, except we are making + // search options required here and returning a promise instead of observable. + search: < + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse + >( + request: SearchStrategyRequest, + options: ISearchOptions + ) => Promise; } /** @public **/ diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 0e21f6f695551..9d417684b1651 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -50,7 +50,7 @@ import { COMPARE_ALL_OPTIONS, } from '../common'; -import { FilterLabel } from './ui/filter_bar'; +import { FilterLabel } from './ui'; import { generateFilters, @@ -421,10 +421,9 @@ export { SearchBar, SearchBarProps, StatefulSearchBarProps, - FilterBar, + IndexPatternSelectProps, QueryStringInput, QueryStringInputProps, - IndexPatternSelect, } from './ui'; /** diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index b8eb7a556ac44..13f658b562d39 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -5,7 +5,6 @@ ```ts import { $Values } from '@kbn/utility-types'; -import _ from 'lodash'; import { Action } from 'history'; import { ApiResponse } from '@elastic/elasticsearch'; import { ApiResponse as ApiResponse_2 } from '@elastic/elasticsearch/lib/Transport'; @@ -13,7 +12,6 @@ import { ApplicationStart } from 'kibana/public'; import { Assign } from '@kbn/utility-types'; import { BehaviorSubject } from 'rxjs'; import Boom from 'boom'; -import { Component } from 'react'; import { CoreSetup } from 'src/core/public'; import { CoreSetup as CoreSetup_2 } from 'kibana/public'; import { CoreStart } from 'kibana/public'; @@ -65,7 +63,7 @@ import { RequestStatistics } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; -import { SavedObject as SavedObject_3 } from 'src/core/public'; +import { SavedObject as SavedObject_2 } from 'src/core/public'; import { SavedObjectReference as SavedObjectReference_2 } from 'src/core/types'; import { SavedObjectsClientContract } from 'src/core/public'; import { Search } from '@elastic/elasticsearch/api/requestParams'; @@ -447,8 +445,6 @@ export interface DataPublicPluginStartActions { // // @public export interface DataPublicPluginStartUi { - // Warning: (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts - // // (undocumented) IndexPatternSelect: React.ComponentType; // (undocumented) @@ -547,7 +543,7 @@ export type EsdslExpressionFunctionDefinition = ExpressionFunctionDefinition JSX.Element; FILTERS: typeof FILTERS; FilterStateStore: typeof FilterStateStore; buildEmptyFilter: (isPinned: boolean, index?: string | undefined) => import("../common").Filter; @@ -805,14 +801,6 @@ export type Filter = { query?: any; }; -// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "FilterBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const FilterBar: React.ComponentClass, any> & { - WrappedComponent: React.ComponentType; -}; - // Warning: (ae-missing-release-tag) "FilterManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1299,32 +1287,15 @@ export const indexPatterns: { // @public (undocumented) export type IndexPatternsContract = PublicMethodsOf; -// Warning: (ae-missing-release-tag) "IndexPatternSelect" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "IndexPatternSelectProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class IndexPatternSelect extends Component { - constructor(props: IndexPatternSelectProps); - // (undocumented) - componentDidMount(): void; - // (undocumented) - componentWillUnmount(): void; - // (undocumented) - debouncedFetch: ((searchValue: string) => Promise) & _.Cancelable; - // (undocumented) - fetchOptions: (searchValue?: string) => void; - // (undocumented) - fetchSelectedIndexPattern: (indexPatternId: string) => Promise; - // (undocumented) - onChange: (selectedOptions: any) => void; - // (undocumented) - render(): JSX.Element; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSelectState" needs to be exported by the entry point index.d.ts - // - // (undocumented) - state: IndexPatternSelectState; - // (undocumented) - UNSAFE_componentWillReceiveProps(nextProps: IndexPatternSelectProps): void; -} +export type IndexPatternSelectProps = Required, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder'> & { + indexPatternId: string; + fieldTypes?: string[]; + onNoIndexPatterns?: () => void; + savedObjectsClient: SavedObjectsClientContract; +}; // Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1740,7 +1711,7 @@ export interface QueryStateChange extends QueryStateChangePartial { // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC; +export const QueryStringInput: (props: QueryStringInputProps) => JSX.Element; // Warning: (ae-missing-release-tag) "QueryStringInputProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2315,7 +2286,6 @@ export const UI_SETTINGS: { // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "generateFilters" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/search/aggs/aggs_service.ts b/src/plugins/data/public/search/aggs/aggs_service.ts index d535f97fefdf8..4b088ddfe314f 100644 --- a/src/plugins/data/public/search/aggs/aggs_service.ts +++ b/src/plugins/data/public/search/aggs/aggs_service.ts @@ -21,17 +21,16 @@ import { Subscription } from 'rxjs'; import { IUiSettingsClient } from 'src/core/public'; import { ExpressionsServiceSetup } from 'src/plugins/expressions/common'; +import { FieldFormatsStart } from '../../field_formats'; +import { getForceNow } from '../../query/timefilter/lib/get_force_now'; +import { calculateBounds, TimeRange } from '../../../common'; import { aggsRequiredUiSettings, AggsCommonStartDependencies, AggsCommonService, AggConfigs, AggTypesDependencies, - calculateBounds, - TimeRange, -} from '../../../common'; -import { FieldFormatsStart } from '../../field_formats'; -import { getForceNow } from '../../query/timefilter/lib/get_force_now'; +} from '../../../common/search/aggs'; import { AggsSetup, AggsStart } from './types'; /** diff --git a/src/plugins/data/public/search/errors/timeout_error.tsx b/src/plugins/data/public/search/errors/timeout_error.tsx index a9ff0c3b38ae6..007689dd0269d 100644 --- a/src/plugins/data/public/search/errors/timeout_error.tsx +++ b/src/plugins/data/public/search/errors/timeout_error.tsx @@ -97,7 +97,12 @@ export class SearchTimeoutError extends KbnError { <> - this.onClick(application)} size="s"> + this.onClick(application)} + size="s" + data-test-subj="searchTimeoutError" + > {actionText} diff --git a/src/plugins/data/public/search/fetch/handle_response.tsx b/src/plugins/data/public/search/fetch/handle_response.tsx index 14e9b59f49bfb..93b1fc1373528 100644 --- a/src/plugins/data/public/search/fetch/handle_response.tsx +++ b/src/plugins/data/public/search/fetch/handle_response.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; import { SearchResponse } from 'elasticsearch'; -import { ShardFailureOpenModalButton } from '../../ui'; +import { ShardFailureOpenModalButton } from '../../ui/shard_failure_modal'; import { toMountPoint } from '../../../../kibana_react/public'; import { getNotifications } from '../../services'; import { SearchRequest } from '..'; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 5014418d6b70b..2d582b30bcd14 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -24,6 +24,9 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './types'; import { handleResponse } from './fetch'; import { IEsSearchRequest, + IEsSearchResponse, + IKibanaSearchRequest, + IKibanaSearchResponse, ISearchGeneric, ISearchOptions, SearchSourceService, @@ -117,8 +120,14 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), - search: (request: IEsSearchRequest, options: ISearchOptions) => { - return search(request, options).toPromise(); + search: < + SearchStrategyRequest extends IKibanaSearchRequest = IEsSearchRequest, + SearchStrategyResponse extends IKibanaSearchResponse = IEsSearchResponse + >( + request: SearchStrategyRequest, + options: ISearchOptions + ) => { + return search(request, options).toPromise() as Promise; }, onResponse: handleResponse, legacy: { diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 7b5d79aff24ef..21a03a49fe058 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -27,9 +27,8 @@ import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; -import { IndexPatternSelectProps } from './ui/index_pattern_select'; import { IndexPatternsContract } from './index_patterns'; -import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar'; +import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup } from '../../usage_collection/public'; export interface DataPublicPluginEnhancements { 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 33928e4f87afc..d7fb6e4c64922 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 @@ -46,7 +46,9 @@ interface State { isFilterSelected: boolean[]; } -export class ApplyFiltersPopoverContent extends Component { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default class ApplyFiltersPopoverContent extends Component { public static defaultProps = { filters: [], }; diff --git a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx index cffcad66cbc24..80e1a26163b72 100644 --- a/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx +++ b/src/plugins/data/public/ui/apply_filters/apply_filters_popover.tsx @@ -18,12 +18,20 @@ */ import React from 'react'; -import { ApplyFiltersPopoverContent } from './apply_filter_popover_content'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; import { IIndexPattern, Filter } from '../..'; type CancelFnType = () => void; type SubmitFnType = (filters: Filter[]) => void; +const Fallback = () => ( + + + +); + +const LazyApplyFiltersPopoverContent = React.lazy(() => import('./apply_filter_popover_content')); + export const applyFiltersPopover = ( filters: Filter[], indexPatterns: IIndexPattern[], @@ -31,11 +39,13 @@ export const applyFiltersPopover = ( onSubmit: SubmitFnType ) => { return ( - + }> + + ); }; diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss index 73ec14de82b43..b79b7c038f9cc 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_item.scss @@ -33,8 +33,6 @@ } .globalFilterItem-isError, .globalFilterItem-isWarning { - text-decoration: none; - .globalFilterLabel__value { font-weight: $euiFontWeightBold; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx index 59afc1606adf9..7065a05193e6d 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.test.tsx @@ -18,12 +18,10 @@ */ import React from 'react'; -import { FilterLabel } from './filter_label'; -import { render, cleanup } from '@testing-library/react/pure'; +import FilterLabel from './filter_label'; +import { render } from '@testing-library/react'; import { phraseFilter } from '../../../../stubs'; -afterEach(cleanup); - test('alias', () => { const filter = { ...phraseFilter, diff --git a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx index 3b85d0753b8c5..c27d7422dc911 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_editor/lib/filter_label.tsx @@ -24,13 +24,16 @@ import { existsOperator, isOneOfOperator } from './filter_operators'; import { Filter, FILTERS } from '../../../../../common'; import type { FilterLabelStatus } from '../../filter_item'; -interface Props { +// @internal +export interface FilterLabelProps { filter: Filter; valueLabel?: string; filterLabelStatus?: FilterLabelStatus; } -export function FilterLabel({ filter, valueLabel, filterLabelStatus }: Props) { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default function FilterLabel({ filter, valueLabel, filterLabelStatus }: FilterLabelProps) { const prefixText = filter.meta.negate ? ` ${i18n.translate('data.filter.filterBar.negatedFilterPrefix', { defaultMessage: 'NOT ', diff --git a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx index 3f9cbce06b315..d61b722fd1b87 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_view/index.tsx @@ -20,7 +20,7 @@ import { EuiBadge, useInnerText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { FC } from 'react'; -import { FilterLabel } from '../filter_editor/lib/filter_label'; +import { FilterLabel } from '../'; import { Filter, isFilterPinned } from '../../../../common'; import type { FilterLabelStatus } from '../filter_item'; diff --git a/src/plugins/data/public/ui/filter_bar/index.tsx b/src/plugins/data/public/ui/filter_bar/index.tsx new file mode 100644 index 0000000000000..b4296bb6615d4 --- /dev/null +++ b/src/plugins/data/public/ui/filter_bar/index.tsx @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import type { FilterLabelProps } from './filter_editor/lib/filter_label'; + +const Fallback = () => ( + + + +); + +const LazyFilterLabel = React.lazy(() => import('./filter_editor/lib/filter_label')); +export const FilterLabel = (props: FilterLabelProps) => ( + }> + + +); diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 299b9d2681578..87f515afbee41 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -17,17 +17,8 @@ * under the License. */ -export { SuggestionsComponent } from './typeahead'; -export { IndexPatternSelect } from './index_pattern_select'; -export { FilterBar } from './filter_bar'; -export { QueryStringInput, QueryStringInputProps } from './query_string_input/query_string_input'; +export { IndexPatternSelectProps } from './index_pattern_select'; +export { FilterLabel } from './filter_bar'; +export { QueryStringInput, QueryStringInputProps } from './query_string_input'; export { SearchBar, SearchBarProps, StatefulSearchBarProps } from './search_bar'; - -// @internal -export { ShardFailureOpenModalButton, ShardFailureRequest } from './shard_failure_modal'; - -// @internal -export { SavedQueryManagementComponent } from './saved_query_management'; - -// @internal -export { SaveQueryForm, SavedQueryMeta } from './saved_query_form'; +export { SuggestionsComponent } from './typeahead'; diff --git a/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx new file mode 100644 index 0000000000000..b49cc24ba90b1 --- /dev/null +++ b/src/plugins/data/public/ui/index_pattern_select/create_index_pattern_select.tsx @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import _ from 'lodash'; +import React from 'react'; + +import { SavedObjectsClientContract } from 'src/core/public'; +import { IndexPatternSelect, IndexPatternSelectProps } from './'; + +// Takes in stateful runtime dependencies and pre-wires them to the component +export function createIndexPatternSelect(savedObjectsClient: SavedObjectsClientContract) { + return (props: Omit) => ( + + ); +} diff --git a/src/plugins/data/public/ui/index_pattern_select/index.tsx b/src/plugins/data/public/ui/index_pattern_select/index.tsx new file mode 100644 index 0000000000000..2912ec401b8b6 --- /dev/null +++ b/src/plugins/data/public/ui/index_pattern_select/index.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import type { IndexPatternSelectProps } from './index_pattern_select'; + +const Fallback = () => ( + + + +); + +const LazyIndexPatternSelect = React.lazy(() => import('./index_pattern_select')); +export const IndexPatternSelect = (props: IndexPatternSelectProps) => ( + }> + + +); + +export * from './create_index_pattern_select'; +export type { IndexPatternSelectProps } from './index_pattern_select'; diff --git a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx index f187dcb804c79..8de1e2c16f159 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx +++ b/src/plugins/data/public/ui/index_pattern_select/index_pattern_select.tsx @@ -23,11 +23,10 @@ import React, { Component } from 'react'; import { Required } from '@kbn/utility-types'; import { EuiComboBox, EuiComboBoxProps } from '@elastic/eui'; -import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; +import { SavedObjectsClientContract, SimpleSavedObject } from 'src/core/public'; import { getTitle } from '../../../common/index_patterns/lib'; export type IndexPatternSelectProps = Required< - // Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions' | 'append' | 'prepend' | 'sortMatchesBy'>, Omit, 'isLoading' | 'onSearchChange' | 'options' | 'selectedOptions'>, 'onChange' | 'placeholder' > & { @@ -59,14 +58,9 @@ const getIndexPatterns = async ( return resp.savedObjects; }; -// Takes in stateful runtime dependencies and pre-wires them to the component -export function createIndexPatternSelect(savedObjectsClient: SavedObjectsClientContract) { - return (props: Omit) => ( - - ); -} - -export class IndexPatternSelect extends Component { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default class IndexPatternSelect extends Component { private isMounted: boolean = false; state: IndexPatternSelectState; diff --git a/src/plugins/data/public/ui/query_string_input/index.tsx b/src/plugins/data/public/ui/query_string_input/index.tsx new file mode 100644 index 0000000000000..5bc5bd5097969 --- /dev/null +++ b/src/plugins/data/public/ui/query_string_input/index.tsx @@ -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 React from 'react'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import { withKibana } from '../../../../kibana_react/public'; +import type { QueryBarTopRowProps } from './query_bar_top_row'; +import type { QueryStringInputProps } from './query_string_input'; + +const Fallback = () => ( + + + +); + +const LazyQueryBarTopRow = React.lazy(() => import('./query_bar_top_row')); +export const QueryBarTopRow = (props: QueryBarTopRowProps) => ( + }> + + +); + +const LazyQueryStringInputUI = withKibana(React.lazy(() => import('./query_string_input'))); +export const QueryStringInput = (props: QueryStringInputProps) => ( + }> + + +); +export type { QueryStringInputProps }; diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index 879ff6708068e..960be8ba99f08 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -21,7 +21,10 @@ import { mockPersistedLogFactory } from './query_string_input.test.mocks'; import React from 'react'; import { mount } from 'enzyme'; -import { QueryBarTopRow } from './query_bar_top_row'; +import { waitFor } from '@testing-library/dom'; +import { render } from '@testing-library/react'; + +import { QueryBarTopRow } from './'; import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; @@ -120,8 +123,8 @@ describe('QueryBarTopRowTopRow', () => { jest.clearAllMocks(); }); - it('Should render query and time picker', () => { - const component = mount( + it('Should render query and time picker', async () => { + const { getByText, getByTestId } = render( wrapQueryBarTopRowInContext({ query: kqlQuery, screenTitle: 'Another Screen', @@ -131,8 +134,8 @@ describe('QueryBarTopRowTopRow', () => { }) ); - expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1); - expect(component.find(TIMEPICKER_SELECTOR).length).toBe(1); + await waitFor(() => getByText(kqlQuery.query)); + await waitFor(() => getByTestId('superDatePickerShowDatesButton')); }); it('Should create a unique PersistedLog based on the appName and query language', () => { diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index e5d03e2a774f1..e01fbedbe38de 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -37,12 +37,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useKibana, toMountPoint } from '../../../../kibana_react/public'; -import { QueryStringInput } from './query_string_input'; +import { QueryStringInput } from './'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; -interface Props { +// @internal +export interface QueryBarTopRowProps { query?: Query; onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; @@ -67,7 +68,9 @@ interface Props { indicateNoData?: boolean; } -export function QueryBarTopRow(props: Props) { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default function QueryBarTopRow(props: QueryBarTopRowProps) { const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); const [isQueryInputFocused, setIsQueryInputFocused] = useState(false); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index bb0a8b848c89e..7d0ad4b3be097 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -23,18 +23,24 @@ import { mockPersistedLogFactory, } from './query_string_input.test.mocks'; -import { EuiTextArea } from '@elastic/eui'; import React from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { mount } from 'enzyme'; +import { waitFor } from '@testing-library/dom'; +import { render } from '@testing-library/react'; + +import { EuiTextArea } from '@elastic/eui'; + import { QueryLanguageSwitcher } from './language_switcher'; -import { QueryStringInput, QueryStringInputUI } from './query_string_input'; +import { QueryStringInput } from './'; +import type QueryStringInputUI from './query_string_input'; + import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; -const startMock = coreMock.createStart(); import { stubIndexPatternWithFields } from '../../stubs'; - import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; -import { I18nProvider } from '@kbn/i18n/react'; -import { mount } from 'enzyme'; + +const startMock = coreMock.createStart(); const noop = () => { return; @@ -94,16 +100,17 @@ describe('QueryStringInput', () => { jest.clearAllMocks(); }); - it('Should render the given query', () => { - const component = mount( + it('Should render the given query', async () => { + const { getByText } = render( wrapQueryStringInputInContext({ query: kqlQuery, onSubmit: noop, indexPatterns: [stubIndexPatternWithFields], }) ); - expect(component.find(EuiTextArea).props().value).toBe(kqlQuery.query); - expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(kqlQuery.language); + + await waitFor(() => getByText(kqlQuery.query)); + await waitFor(() => getByText('KQL')); }); it('Should pass the query language to the language switcher', () => { diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index ec45b80927f6d..c17872028ea8d 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -39,7 +39,7 @@ import { Toast } from 'src/core/public'; import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; -import { withKibana, KibanaReactContextValue, toMountPoint } from '../../../../kibana_react/public'; +import { KibanaReactContextValue, toMountPoint } from '../../../../kibana_react/public'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; @@ -93,7 +93,9 @@ const KEY_CODES = { END: 35, }; -export class QueryStringInputUI extends Component { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default class QueryStringInputUI extends Component { public state: State = { isSuggestionsVisible: false, index: null, @@ -546,7 +548,7 @@ export class QueryStringInputUI extends Component { public componentWillUnmount() { if (this.abortController) this.abortController.abort(); - this.updateSuggestions.cancel(); + if (this.updateSuggestions.cancel) this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); window.removeEventListener('scroll', this.handleListUpdate, { capture: true }); @@ -690,5 +692,3 @@ export class QueryStringInputUI extends Component { ); } } - -export const QueryStringInput: React.FC = withKibana(QueryStringInputUI); diff --git a/src/plugins/data/public/ui/saved_query_form/index.ts b/src/plugins/data/public/ui/saved_query_form/index.ts index c52b6c92ef6d3..d3326ee0d3118 100644 --- a/src/plugins/data/public/ui/saved_query_form/index.ts +++ b/src/plugins/data/public/ui/saved_query_form/index.ts @@ -17,4 +17,5 @@ * under the License. */ +// @internal export { SavedQueryMeta, SaveQueryForm } from '../saved_query_form/save_query_form'; diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 9f0ba2378592a..48e2e8dab7580 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -23,7 +23,7 @@ import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { KibanaContextProvider } from '../../../../kibana_react/public'; import { QueryStart, SavedQuery } from '../../query'; -import { SearchBarOwnProps, SearchBar } from './search_bar'; +import { SearchBar, SearchBarOwnProps } from './'; import { useFilterManager } from './lib/use_filter_manager'; import { useTimefilter } from './lib/use_timefilter'; import { useSavedQuery } from './lib/use_saved_query'; diff --git a/src/plugins/data/public/ui/search_bar/index.tsx b/src/plugins/data/public/ui/search_bar/index.tsx index fbc9f4a41ebbf..d81ed7333655d 100644 --- a/src/plugins/data/public/ui/search_bar/index.tsx +++ b/src/plugins/data/public/ui/search_bar/index.tsx @@ -17,5 +17,25 @@ * under the License. */ -export { SearchBar, SearchBarProps } from './search_bar'; +import React from 'react'; +import { injectI18n } from '@kbn/i18n/react'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import { withKibana } from '../../../../kibana_react/public'; +import type { SearchBarProps } from './search_bar'; + +const Fallback = () => ( + + + +); + +const LazySearchBar = React.lazy(() => import('./search_bar')); +const WrappedSearchBar = (props: SearchBarProps) => ( + }> + + +); + +export const SearchBar = injectI18n(withKibana(WrappedSearchBar)); export { StatefulSearchBarProps } from './create_search_bar'; +export type { SearchBarProps, SearchBarOwnProps } from './search_bar'; diff --git a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx index cf438eaa0e972..a89b9bb7f91ef 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.test.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.test.tsx @@ -18,7 +18,10 @@ */ import React from 'react'; -import { SearchBar } from './search_bar'; +import { waitFor } from '@testing-library/dom'; +import { render } from '@testing-library/react'; + +import { SearchBar } from './'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -26,7 +29,6 @@ import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from '../../../../../core/public/mocks'; const startMock = coreMock.createStart(); -import { mount } from 'enzyme'; import { IIndexPattern } from '../..'; const mockTimeHistory = { @@ -41,7 +43,7 @@ jest.mock('..', () => { }; }); -jest.mock('../query_string_input/query_bar_top_row', () => { +jest.mock('../query_string_input', () => { return { QueryBarTopRow: () =>
, }; @@ -115,42 +117,48 @@ function wrapSearchBarInContext(testProps: any) { ); } -describe('SearchBar', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79910 +describe.skip('SearchBar', () => { + const SEARCH_BAR_TEST_ID = 'globalQueryBar'; const SEARCH_BAR_ROOT = '.globalQueryBar'; - const FILTER_BAR = '.filterBar'; + const FILTER_BAR = '.globalFilterBar'; const QUERY_BAR = '.queryBar'; beforeEach(() => { jest.clearAllMocks(); }); - it('Should render query bar when no options provided (in reality - timepicker)', () => { - const component = mount( + it('Should render query bar when no options provided (in reality - timepicker)', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); }); - it('Should render empty when timepicker is off and no options provided', () => { - const component = mount( + it('Should render empty when timepicker is off and no options provided', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); }); - it('Should render filter bar, when required fields are provided', () => { - const component = mount( + it('Should render filter bar, when required fields are provided', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showDatePicker: false, @@ -159,13 +167,15 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(0); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); }); - it('Should NOT render filter bar, if disabled', () => { - const component = mount( + it('Should NOT render filter bar, if disabled', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], showFilterBar: false, @@ -175,13 +185,15 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); }); - it('Should render query bar, when required fields are provided', () => { - const component = mount( + it('Should render query bar, when required fields are provided', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -190,13 +202,15 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(1); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); }); - it('Should NOT render query bar, if disabled', () => { - const component = mount( + it('Should NOT render query bar, if disabled', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -206,13 +220,15 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(0); - expect(component.find(QUERY_BAR).length).toBe(0); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(0); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(0); }); - it('Should render query bar and filter bar', () => { - const component = mount( + it('Should render query bar and filter bar', async () => { + const { container, getByTestId } = render( wrapSearchBarInContext({ indexPatterns: [mockIndexPattern], screenTitle: 'test screen', @@ -223,8 +239,10 @@ describe('SearchBar', () => { }) ); - expect(component.find(SEARCH_BAR_ROOT).length).toBe(1); - expect(component.find(FILTER_BAR).length).toBe(1); - expect(component.find(QUERY_BAR).length).toBe(1); + await waitFor(() => getByTestId(SEARCH_BAR_TEST_ID)); + + expect(container.querySelectorAll(SEARCH_BAR_ROOT).length).toBe(1); + expect(container.querySelectorAll(FILTER_BAR).length).toBe(1); + expect(container.querySelectorAll(QUERY_BAR).length).toBe(1); }); }); diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index b18b2fa9f0418..95651ac9ed8b3 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -26,11 +26,13 @@ import { get, isEqual } from 'lodash'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; -import { QueryBarTopRow } from '../query_string_input/query_bar_top_row'; +import { QueryBarTopRow } from '../query_string_input'; import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; import { IDataPluginServices } from '../../types'; import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; -import { SavedQueryMeta, SavedQueryManagementComponent, SaveQueryForm, FilterBar } from '..'; +import { FilterBar } from '../filter_bar/filter_bar'; +import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; +import { SavedQueryManagementComponent } from '../saved_query_management'; interface SearchBarInjectedDeps { kibana: KibanaReactContextValue; @@ -437,7 +439,7 @@ class SearchBarUI extends Component { } return ( -
+
{queryBar} {filterBar} @@ -465,4 +467,6 @@ class SearchBarUI extends Component { } } -export const SearchBar = injectI18n(withKibana(SearchBarUI)); +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default injectI18n(withKibana(SearchBarUI)); diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.tsx b/src/plugins/data/public/ui/shard_failure_modal/index.tsx new file mode 100644 index 0000000000000..cea882deff365 --- /dev/null +++ b/src/plugins/data/public/ui/shard_failure_modal/index.tsx @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import type { ShardFailureOpenModalButtonProps } from './shard_failure_open_modal_button'; + +const Fallback = () => ( + + + +); + +const LazyShardFailureOpenModalButton = React.lazy( + () => import('./shard_failure_open_modal_button') +); +export const ShardFailureOpenModalButton = (props: ShardFailureOpenModalButtonProps) => ( + }> + + +); + +export type { ShardFailureRequest } from './shard_failure_types'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx index aee8d1f4eac4d..7a4ca80e6192f 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.test.tsx @@ -19,7 +19,7 @@ import { openModal } from './shard_failure_open_modal_button.test.mocks'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ShardFailureOpenModalButton } from './shard_failure_open_modal_button'; +import ShardFailureOpenModalButton from './shard_failure_open_modal_button'; import { shardFailureRequest } from './__mocks__/shard_failure_request'; import { shardFailureResponse } from './__mocks__/shard_failure_response'; import { findTestSubject } from '@elastic/eui/lib/test'; diff --git a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx index 9d89dc4cb1a29..b6bbba31dc5f0 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx +++ b/src/plugins/data/public/ui/shard_failure_modal/shard_failure_open_modal_button.tsx @@ -26,13 +26,20 @@ import { toMountPoint } from '../../../../kibana_react/public'; import { ShardFailureModal } from './shard_failure_modal'; import { ShardFailureRequest } from './shard_failure_types'; -interface Props { +// @internal +export interface ShardFailureOpenModalButtonProps { request: ShardFailureRequest; response: SearchResponse; title: string; } -export function ShardFailureOpenModalButton({ request, response, title }: Props) { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default function ShardFailureOpenModalButton({ + request, + response, + title, +}: ShardFailureOpenModalButtonProps) { function onClick() { const modal = getOverlays().openModal( toMountPoint( diff --git a/src/plugins/data/public/ui/typeahead/index.tsx b/src/plugins/data/public/ui/typeahead/index.tsx new file mode 100644 index 0000000000000..aa3c2d71300df --- /dev/null +++ b/src/plugins/data/public/ui/typeahead/index.tsx @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiLoadingContent, EuiDelayRender } from '@elastic/eui'; +import type { SuggestionsComponentProps } from './suggestions_component'; + +const Fallback = () => ( + + + +); + +const LazySuggestionsComponent = React.lazy(() => import('./suggestions_component')); +export const SuggestionsComponent = (props: SuggestionsComponentProps) => ( + }> + + +); diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index 583940015c152..d813a5281c448 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -21,7 +21,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; import { SuggestionComponent } from './suggestion_component'; -import { SuggestionsComponent } from './suggestions_component'; +import SuggestionsComponent from './suggestions_component'; const noop = () => { return; diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index 50ed9e9542d36..7ec8d0d9b720e 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -29,7 +29,8 @@ import { SUGGESTIONS_LIST_REQUIRED_WIDTH, } from './constants'; -interface Props { +// @internal +export interface SuggestionsComponentProps { index: number | null; onClick: (suggestion: QuerySuggestion) => void; onMouseEnter: (index: number) => void; @@ -42,7 +43,9 @@ interface Props { export type SuggestionsListSize = 's' | 'l'; -export class SuggestionsComponent extends Component { +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default class SuggestionsComponent extends Component { private childNodes: HTMLDivElement[] = []; private parentNode: HTMLDivElement | null = null; @@ -107,7 +110,7 @@ export class SuggestionsComponent extends Component { ); } - public componentDidUpdate(prevProps: Props) { + public componentDidUpdate(prevProps: SuggestionsComponentProps) { if (prevProps.index !== this.props.index) { this.scrollIntoView(); } diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index aac1fe1fde212..11dcbb01bf4a6 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -214,11 +214,13 @@ export { ISearchSetup, ISearchStart, toSnakeCase, + getAsyncOptions, getDefaultSearchParams, getShardTimeout, getTotalLoaded, shimHitsTotal, usageProvider, + shimAbortSignal, SearchUsage, } from './search'; 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 e2ed500689cfa..6e185d30ad56a 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 @@ -23,7 +23,13 @@ import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; import { toSnakeCase } from './to_snake_case'; -import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded, getShardTimeout } from '..'; +import { + ISearchStrategy, + getDefaultSearchParams, + getTotalLoaded, + getShardTimeout, + shimAbortSignal, +} from '..'; export const esSearchStrategyProvider = ( config$: Observable, @@ -52,10 +58,10 @@ export const esSearchStrategyProvider = ( }); try { - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - const promise = context.core.elasticsearch.client.asCurrentUser.search(params); - if (options?.abortSignal) - options.abortSignal.addEventListener('abort', () => promise.abort()); + const promise = shimAbortSignal( + context.core.elasticsearch.client.asCurrentUser.search(params), + options?.abortSignal + ); const { body: rawResponse } = (await promise) as ApiResponse>; if (usage) usage.trackSuccess(rawResponse.took); diff --git a/src/plugins/data/server/search/es_search/get_default_search_params.ts b/src/plugins/data/server/search/es_search/get_default_search_params.ts index 13607fce51670..b51293b88fcec 100644 --- a/src/plugins/data/server/search/es_search/get_default_search_params.ts +++ b/src/plugins/data/server/search/es_search/get_default_search_params.ts @@ -42,3 +42,11 @@ export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient trackTotalHits: true, }; } + +/** + @internal + */ +export const getAsyncOptions = () => ({ + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute +}); diff --git a/src/plugins/data/server/search/es_search/index.ts b/src/plugins/data/server/search/es_search/index.ts index 1bd17fc986168..63ab7a025ee51 100644 --- a/src/plugins/data/server/search/es_search/index.ts +++ b/src/plugins/data/server/search/es_search/index.ts @@ -21,5 +21,6 @@ export { esSearchStrategyProvider } from './es_search_strategy'; export * from './get_default_search_params'; export { getTotalLoaded } from './get_total_loaded'; export * from './to_snake_case'; +export { shimAbortSignal } from './shim_abort_signal'; export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common'; diff --git a/src/plugins/data/server/search/es_search/shim_abort_signal.test.ts b/src/plugins/data/server/search/es_search/shim_abort_signal.test.ts new file mode 100644 index 0000000000000..794b6535cc184 --- /dev/null +++ b/src/plugins/data/server/search/es_search/shim_abort_signal.test.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 { elasticsearchServiceMock } from '../../../../../core/server/mocks'; +import { shimAbortSignal } from '.'; + +describe('shimAbortSignal', () => { + it('aborts the promise if the signal is aborted', () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + shimAbortSignal(promise, controller.signal); + controller.abort(); + + expect(promise.abort).toHaveBeenCalled(); + }); + + it('returns the original promise', async () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + const response = await shimAbortSignal(promise, controller.signal); + + expect(response).toEqual(expect.objectContaining({ body: { success: true } })); + }); + + it('allows the promise to be aborted manually', () => { + const promise = elasticsearchServiceMock.createSuccessTransportRequestPromise({ + success: true, + }); + const controller = new AbortController(); + const enhancedPromise = shimAbortSignal(promise, controller.signal); + + enhancedPromise.abort(); + expect(promise.abort).toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/data/server/search/es_search/shim_abort_signal.ts b/src/plugins/data/server/search/es_search/shim_abort_signal.ts new file mode 100644 index 0000000000000..14a4a6919c5af --- /dev/null +++ b/src/plugins/data/server/search/es_search/shim_abort_signal.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 { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; + +/** + * + * @internal + * NOTE: Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + * is resolved + * + * @param promise a TransportRequestPromise + * @param signal optional AbortSignal + * + * @returns a TransportRequestPromise that will be aborted if the signal is aborted + */ +export const shimAbortSignal = >( + promise: T, + signal: AbortSignal | undefined +): T => { + if (signal) { + signal.addEventListener('abort', () => promise.abort()); + } + return promise; +}; diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts index 764dcd189f8db..8103b680c6bbb 100644 --- a/src/plugins/data/server/search/routes/call_msearch.ts +++ b/src/plugins/data/server/search/routes/call_msearch.ts @@ -25,7 +25,7 @@ import { IUiSettingsClient, IScopedClusterClient, SharedGlobalConfig } from 'src import { MsearchRequestBody, MsearchResponse } from '../../../common/search/search_source'; import { shimHitsTotal } from './shim_hits_total'; -import { getShardTimeout, getDefaultSearchParams, toSnakeCase } from '..'; +import { getShardTimeout, getDefaultSearchParams, toSnakeCase, shimAbortSignal } from '..'; /** @internal */ export function convertRequestBody( @@ -74,18 +74,17 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) { const body = convertRequestBody(params.body, timeout); - // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 - const promise = esClient.asCurrentUser.msearch( - { - body, - }, - { - querystring: toSnakeCase(defaultParams), - } + const promise = shimAbortSignal( + esClient.asCurrentUser.msearch( + { + body, + }, + { + querystring: toSnakeCase(defaultParams), + } + ), + params.signal ); - if (params.signal) { - params.signal.addEventListener('abort', () => promise.abort()); - } const response = (await promise) as ApiResponse<{ responses: Array> }>; return { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index fed0c1a02297e..45dbdee0f846b 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -47,6 +47,7 @@ import { SearchResponse } from 'elasticsearch'; import { SerializedFieldFormat as SerializedFieldFormat_2 } from 'src/plugins/expressions/common'; import { ShardsResponse } from 'elasticsearch'; import { ToastInputFields } from 'src/core/public/notifications'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { Unit } from '@elastic/datemath'; @@ -354,6 +355,12 @@ export type Filter = { query?: any; }; +// @internal (undocumented) +export const getAsyncOptions: () => { + waitForCompletionTimeout: string; + keepAlive: string; +}; + // Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -980,6 +987,9 @@ export interface SearchUsage { trackSuccess(duration: number): Promise; } +// @internal +export const shimAbortSignal: >(promise: T, signal: AbortSignal | undefined) => T; + // @internal export function shimHitsTotal(response: SearchResponse): { hits: { @@ -1115,19 +1125,19 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:229:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:230:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:231:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:242:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:246:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:247:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:254:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // src/plugins/data/server/search/types.ts:78:5 - (ae-forgotten-export) The symbol "ISearchStartSearchSource" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 72030d91220b5..4334af63539e3 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -27,3 +27,4 @@ export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; export const CONTEXT_STEP_SETTING = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; +export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 92b96d11723e0..078a047324113 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -71,7 +71,7 @@ const { import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; import { validateTimeRange } from '../helpers/validate_time_range'; import { popularizeField } from '../helpers/popularize_field'; - +import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; import { @@ -80,6 +80,7 @@ import { SORT_DEFAULT_ORDER_SETTING, SEARCH_ON_PAGE_LOAD_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, + MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; const fetchStatuses = { @@ -253,6 +254,11 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise if (!_.isEqual(newStatePartial, oldStatePartial)) { $scope.$evalAsync(async () => { + if (oldStatePartial.index !== newStatePartial.index) { + //in case of index switch the route has currently to be reloaded, legacy + return; + } + $scope.state = { ...newState }; // detect changes that should trigger fetching of new data @@ -277,8 +283,18 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise }); $scope.setIndexPattern = async (id) => { - await replaceUrlAppState({ index: id }); - $route.reload(); + const nextIndexPattern = await indexPatterns.get(id); + if (nextIndexPattern) { + const nextAppState = getSwitchIndexPatternAppState( + $scope.indexPattern, + nextIndexPattern, + $scope.state.columns, + $scope.state.sort, + config.get(MODIFY_COLUMNS_ON_SWITCH) + ); + await replaceUrlAppState(nextAppState); + $route.reload(); + } }; // update data source when filters update diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index e7fafde2e68d0..17f3199b75b15 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -18,6 +18,7 @@ */ import { find, template } from 'lodash'; +import { stringify } from 'query-string'; import $ from 'jquery'; import rison from 'rison-node'; import '../../doc_viewer'; @@ -25,7 +26,7 @@ import '../../doc_viewer'; import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; -import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; +import { dispatchRenderComplete, url } from '../../../../../../kibana_utils/public'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; @@ -49,7 +50,7 @@ interface LazyScope extends ng.IScope { [key: string]: any; } -export function createTableRowDirective($compile: ng.ICompileService, $httpParamSerializer: any) { +export function createTableRowDirective($compile: ng.ICompileService) { const cellTemplate = template(noWhiteSpace(cellTemplateHtml)); const truncateByHeightTemplate = template(noWhiteSpace(truncateByHeightTemplateHtml)); @@ -108,32 +109,31 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam }); $scope.inlineFilter = function inlineFilter($event: any, type: string) { - const column = $($event.target).data().column; + const column = $($event.currentTarget).data().column; const field = $scope.indexPattern.fields.getByName(column); $scope.filter(field, $scope.flattenedRow[column], type); }; $scope.getContextAppHref = () => { - const path = `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( - $scope.row._id - )}`; const globalFilters: any = getServices().filterManager.getGlobalFilters(); const appFilters: any = getServices().filterManager.getAppFilters(); - const hash = $httpParamSerializer({ - _g: encodeURI( - rison.encode({ + + const hash = stringify( + url.encodeQuery({ + _g: rison.encode({ filters: globalFilters || [], - }) - ), - _a: encodeURI( - rison.encode({ + }), + _a: rison.encode({ columns: $scope.columns, filters: (appFilters || []).map(esFilters.disableFilter), - }) - ), - }); + }), + }), + { encode: false, sort: false } + ); - return `${path}?${hash}`; + return `#/context/${encodeURIComponent($scope.indexPattern.id)}/${encodeURIComponent( + $scope.row._id + )}?${hash}`; }; // create a tr element that lists the value for each *column* diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html index e8c4fceeca7ff..0d17c2ca94cac 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/cell.html @@ -14,24 +14,24 @@ <% if (filterable) { %> + > + > <% } %> diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.ts new file mode 100644 index 0000000000000..d35346ed24737 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.test.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 { getSwitchIndexPatternAppState } from './get_switch_index_pattern_app_state'; +import { IIndexPatternFieldList, IndexPattern } from '../../../../data/common/index_patterns'; + +const currentIndexPattern: IndexPattern = { + id: 'prev', + getFieldByName(name) { + return this.fields.getByName(name); + }, + fields: { + getByName: (name: string) => { + const fields = [ + { name: 'category', sortable: true }, + { name: 'name', sortable: true }, + ] as IIndexPatternFieldList; + return fields.find((field) => field.name === name); + }, + }, +} as IndexPattern; + +const nextIndexPattern = { + id: 'next', + getFieldByName(name) { + return this.fields.getByName(name); + }, + fields: { + getByName: (name: string) => { + const fields = [{ name: 'category', sortable: true }] as IIndexPatternFieldList; + return fields.find((field) => field.name === name); + }, + }, +} as IndexPattern; + +describe('Discover getSwitchIndexPatternAppState', () => { + test('removing fields that are not part of the next index pattern, keeping unknown fields ', async () => { + const result = getSwitchIndexPatternAppState( + currentIndexPattern, + nextIndexPattern, + ['category', 'name', 'unknown'], + [['category', 'desc']] + ); + expect(result.columns).toEqual(['category', 'unknown']); + expect(result.sort).toEqual([['category', 'desc']]); + }); + test('removing sorted by fields that are not part of the next index pattern', async () => { + const result = getSwitchIndexPatternAppState( + currentIndexPattern, + nextIndexPattern, + ['name'], + [ + ['category', 'desc'], + ['name', 'asc'], + ] + ); + expect(result.columns).toEqual(['_source']); + expect(result.sort).toEqual([['category', 'desc']]); + }); + test('removing sorted by fields that without modifying columns', async () => { + const result = getSwitchIndexPatternAppState( + currentIndexPattern, + nextIndexPattern, + ['name'], + [ + ['category', 'desc'], + ['name', 'asc'], + ], + false + ); + expect(result.columns).toEqual(['name']); + expect(result.sort).toEqual([['category', 'desc']]); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.ts new file mode 100644 index 0000000000000..458b9b7e066fd --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_switch_index_pattern_app_state.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 { getSortArray } from '../angular/doc_table'; +import { SortPairArr } from '../angular/doc_table/lib/get_sort'; +import { IndexPattern } from '../../kibana_services'; + +/** + * Helper function to remove or adapt the currently selected columns/sort to be valid with the next + * index pattern, returns a new state object + */ +export function getSwitchIndexPatternAppState( + currentIndexPattern: IndexPattern, + nextIndexPattern: IndexPattern, + currentColumns: string[], + currentSort: SortPairArr[], + modifyColumns: boolean = true +) { + const nextColumns = modifyColumns + ? currentColumns.filter( + (column) => + nextIndexPattern.fields.getByName(column) || !currentIndexPattern.fields.getByName(column) + ) + : currentColumns; + const nextSort = getSortArray(currentSort, nextIndexPattern); + return { + index: nextIndexPattern.id, + columns: nextColumns.length ? nextColumns : ['_source'], + sort: nextSort, + }; +} diff --git a/src/plugins/discover/public/register_feature.ts b/src/plugins/discover/public/register_feature.ts index 5443bb261ab10..9a66936233692 100644 --- a/src/plugins/discover/public/register_feature.ts +++ b/src/plugins/discover/public/register_feature.ts @@ -25,6 +25,9 @@ export function registerFeature(home: HomePublicPluginSetup) { title: i18n.translate('discover.discoverTitle', { defaultMessage: 'Discover', }), + subtitle: i18n.translate('discover.discoverSubtitle', { + defaultMessage: 'Search and find insights.', + }), description: i18n.translate('discover.discoverDescription', { defaultMessage: 'Interactively explore your data by querying and filtering raw documents.', }), @@ -32,5 +35,7 @@ export function registerFeature(home: HomePublicPluginSetup) { path: '/app/discover#/', showOnHomePage: false, category: FeatureCatalogueCategory.DATA, + solutionId: 'kibana', + order: 200, }); } diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts index c13550e543ab6..a6e42f956a025 100644 --- a/src/plugins/discover/server/saved_objects/search.ts +++ b/src/plugins/discover/server/saved_objects/search.ts @@ -48,7 +48,7 @@ export const searchSavedObjectType: SavedObjectsType = { hits: { type: 'integer', index: false, doc_values: false }, kibanaSavedObjectMeta: { properties: { - searchSourceJSON: { type: 'text', index: false, doc_values: false }, + searchSourceJSON: { type: 'text', index: false }, }, }, sort: { type: 'keyword', index: false, doc_values: false }, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 3eca11cc584a9..5447b982eef14 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -32,6 +32,7 @@ import { CONTEXT_DEFAULT_SIZE_SETTING, CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING, + MODIFY_COLUMNS_ON_SWITCH, } from '../common'; export const uiSettings: Record = { @@ -163,4 +164,15 @@ export const uiSettings: Record = { category: ['discover'], schema: schema.arrayOf(schema.string()), }, + [MODIFY_COLUMNS_ON_SWITCH]: { + name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', { + defaultMessage: 'Modify columns when changing index patterns', + }), + value: true, + description: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchText', { + defaultMessage: 'Remove columns that not available in the new index pattern.', + }), + category: ['discover'], + schema: schema.boolean(), + }, }; diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx index 1e4604af9dc09..fa4d7f466caee 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_error_label.tsx @@ -40,8 +40,10 @@ export function EmbeddableErrorLabel(props: Props) { return (
- - {labelText} + + + {labelText} +
diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx index ea6a6a78c2b67..7c4724a667433 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_header.tsx @@ -183,7 +183,7 @@ export function PanelHeader({ ) : ( - titleComponent + {titleComponent} ); }; diff --git a/src/plugins/es_ui_shared/static/forms/components/field.tsx b/src/plugins/es_ui_shared/static/forms/components/field.tsx index 78702e902ecf6..217a811168814 100644 --- a/src/plugins/es_ui_shared/static/forms/components/field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/field.tsx @@ -38,6 +38,7 @@ import { SelectField, SuperSelectField, ToggleField, + JsonEditorField, } from './fields'; const mapTypeToFieldComponent: { [key: string]: ComponentType } = { @@ -52,6 +53,7 @@ const mapTypeToFieldComponent: { [key: string]: ComponentType } = { [FIELD_TYPES.SELECT]: SelectField, [FIELD_TYPES.SUPER_SELECT]: SuperSelectField, [FIELD_TYPES.TOGGLE]: ToggleField, + [FIELD_TYPES.JSON]: JsonEditorField, }; export const Field = (props: Props) => { diff --git a/src/plugins/es_ui_shared/static/forms/components/fields/json_editor_field.tsx b/src/plugins/es_ui_shared/static/forms/components/fields/json_editor_field.tsx index fd57e098cf806..e2d80825f397e 100644 --- a/src/plugins/es_ui_shared/static/forms/components/fields/json_editor_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/components/fields/json_editor_field.tsx @@ -23,7 +23,7 @@ import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../public'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../hook_form_lib'; interface Props { - field: FieldHook; + field: FieldHook; euiCodeEditorProps?: { [key: string]: any }; [key: string]: any; } @@ -44,7 +44,7 @@ export const JsonEditorField = ({ field, ...rest }: Props) => { ['validations']; - children: (args: { - items: ArrayItem[]; - error: string | null; - addItem: () => void; - removeItem: (id: number) => void; - moveItem: (sourceIdx: number, destinationIdx: number) => void; - form: FormHook; - }) => JSX.Element; + validations?: FieldConfig['validations']; + children: (formFieldArray: FormArrayField) => JSX.Element; } export interface ArrayItem { @@ -45,6 +38,15 @@ export interface ArrayItem { isNew: boolean; } +export interface FormArrayField { + items: ArrayItem[]; + error: string | null; + addItem: () => void; + removeItem: (id: number) => void; + moveItem: (sourceIdx: number, destinationIdx: number) => void; + form: FormHook; +} + /** * Use UseArray to dynamically add fields to your form. * @@ -71,7 +73,7 @@ export const UseArray = ({ const uniqueId = useRef(0); const form = useFormContext(); - const { getFieldDefaultValue } = form; + const { __getFieldDefaultValue } = form; const getNewItemAtIndex = useCallback( (index: number): ArrayItem => ({ @@ -84,7 +86,7 @@ export const UseArray = ({ const fieldDefaultValue = useMemo(() => { const defaultValues = readDefaultValueOnForm - ? (getFieldDefaultValue(path) as any[]) + ? (__getFieldDefaultValue(path) as any[]) : undefined; const getInitialItemsFromValues = (values: any[]): ArrayItem[] => @@ -97,17 +99,23 @@ export const UseArray = ({ return defaultValues ? getInitialItemsFromValues(defaultValues) : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); - }, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue, getNewItemAtIndex]); + }, [ + path, + initialNumberOfItems, + readDefaultValueOnForm, + __getFieldDefaultValue, + getNewItemAtIndex, + ]); // Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data. // Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle. - const fieldConfigBase: FieldConfig & InternalFieldConfig = { + const fieldConfigBase: FieldConfig & InternalFieldConfig = { defaultValue: fieldDefaultValue, - errorDisplayDelay: 0, + valueChangeDebounceTime: 0, isIncludedInOutput: false, }; - const fieldConfig: FieldConfig & InternalFieldConfig = validations + const fieldConfig: FieldConfig & InternalFieldConfig = validations ? { validations, ...fieldConfigBase } : fieldConfigBase; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx index 6b913f246abbb..a3a0984d4a736 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_field.tsx @@ -19,23 +19,23 @@ import React, { FunctionComponent } from 'react'; -import { FieldHook, FieldConfig } from '../types'; +import { FieldHook, FieldConfig, FormData } from '../types'; import { useField } from '../hooks'; import { useFormContext } from '../form_context'; -export interface Props { +export interface Props { path: string; - config?: FieldConfig; + config?: FieldConfig; defaultValue?: T; - component?: FunctionComponent | 'input'; + component?: FunctionComponent; componentProps?: Record; readDefaultValueOnForm?: boolean; - onChange?: (value: T) => void; - children?: (field: FieldHook) => JSX.Element; + onChange?: (value: I) => void; + children?: (field: FieldHook) => JSX.Element; [key: string]: any; } -function UseFieldComp(props: Props) { +function UseFieldComp(props: Props) { const { path, config, @@ -48,18 +48,16 @@ function UseFieldComp(props: Props) { ...rest } = props; - const form = useFormContext(); + const form = useFormContext(); const ComponentToRender = component ?? 'input'; - // For backward compatibility we merge the "componentProps" prop into the "rest" - const propsToForward = - componentProps !== undefined ? { ...componentProps, ...rest } : { ...rest }; + const propsToForward = { ...componentProps, ...rest }; - const fieldConfig: FieldConfig & { initialValue?: T } = + const fieldConfig: FieldConfig & { initialValue?: T } = config !== undefined ? { ...config } : ({ ...form.__readFieldConfigFromSchema(path), - } as Partial>); + } as Partial>); if (defaultValue !== undefined) { // update the form "defaultValue" ref object so when/if we reset the form we can go back to this value @@ -70,21 +68,12 @@ function UseFieldComp(props: Props) { } else { if (readDefaultValueOnForm) { // Read the field initial value from the "defaultValue" object passed to the form - fieldConfig.initialValue = (form.getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue; + fieldConfig.initialValue = + (form.__getFieldDefaultValue(path) as T) ?? fieldConfig.defaultValue; } } - if (!fieldConfig.path) { - (fieldConfig.path as any) = path; - } else { - if (fieldConfig.path !== path) { - throw new Error( - `Field path mismatch. Got "${path}" but field config has "${fieldConfig.path}".` - ); - } - } - - const field = useField(form, path, fieldConfig, onChange); + const field = useField(form, path, fieldConfig, onChange); // Children prevails over anything else provided. if (children) { @@ -111,9 +100,13 @@ export const UseField = React.memo(UseFieldComp) as typeof UseFieldComp; * Get a component providing some common props for all instances. * @param partialProps Partial props to apply to all instances */ -export function getUseField(partialProps: Partial>) { - return function (props: Partial>) { - const componentProps = { ...partialProps, ...props } as Props; - return {...componentProps} />; +export function getUseField( + partialProps: Partial> +) { + return function ( + props: Partial> + ) { + const componentProps = { ...partialProps, ...props } as Props; + return {...componentProps} />; }; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx index d69527e36249b..20f4608352d94 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_multi_fields.tsx @@ -22,27 +22,27 @@ import React from 'react'; import { UseField, Props as UseFieldProps } from './use_field'; import { FieldHook } from '../types'; -type FieldsArray = Array<{ id: string } & Omit, 'children'>>; +type FieldsArray = Array<{ id: string } & Omit, 'children'>>; -interface Props { - fields: { [key: string]: Exclude, 'children'> }; - children: (fields: { [key: string]: FieldHook }) => JSX.Element; +interface Props { + fields: { [K in keyof T]: Exclude, 'children'> }; + children: (fields: { [K in keyof T]: FieldHook }) => JSX.Element; } -export const UseMultiFields = ({ fields, children }: Props) => { +export function UseMultiFields({ fields, children }: Props) { const fieldsArray = Object.entries(fields).reduce( - (acc, [fieldId, field]) => [...acc, { id: fieldId, ...field }], + (acc, [fieldId, field]) => [...acc, { id: fieldId, ...(field as FieldHook) }], [] as FieldsArray ); - const hookFields: { [key: string]: FieldHook } = {}; + const hookFields: { [K in keyof T]: FieldHook } = {} as any; const renderField = (index: number) => { const { id } = fieldsArray[index]; return ( - + {(field) => { - hookFields[id] = field; + hookFields[id as keyof T] = field; return index === fieldsArray.length - 1 ? children(hookFields) : renderField(index + 1); }} @@ -54,4 +54,4 @@ export const UseMultiFields = ({ fields, children }: Props) => { } return renderField(0); -}; +} diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts index 4056947483107..3a2ffdc3af146 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/constants.ts @@ -30,11 +30,15 @@ export const FIELD_TYPES = { SELECT: 'select', SUPER_SELECT: 'superSelect', MULTI_SELECT: 'multiSelect', + JSON: 'json', }; // Validation types export const VALIDATION_TYPES = { - FIELD: 'field', // Default validation error (on the field value) - ASYNC: 'async', // Returned from asynchronous validations - ARRAY_ITEM: 'arrayItem', // If the field value is an Array, this error would be returned if an _item_ of the array is invalid + /** Default validation error (on the field value) */ + FIELD: 'field', + /** Returned from asynchronous validations */ + ASYNC: 'async', + /** If the field value is an Array, this error type would be returned if an _item_ of the array is invalid */ + ARRAY_ITEM: 'arrayItem', }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx index 0e6a75e9c5065..0670220ccd0c9 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/form_data_context.tsx @@ -22,8 +22,8 @@ import React, { createContext, useContext, useMemo } from 'react'; import { FormData, FormHook } from './types'; import { Subject } from './lib'; -export interface Context { - getFormData$: () => Subject; +export interface Context { + getFormData$: () => Subject; getFormData: FormHook['getFormData']; } diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index bb4aae6eccae8..7b21b6638aeac 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -19,7 +19,14 @@ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; -import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types'; +import { + FormHook, + FieldHook, + FieldConfig, + FieldValidateResponse, + ValidationError, + FormData, +} from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; export interface InternalFieldConfig { @@ -27,11 +34,11 @@ export interface InternalFieldConfig { isIncludedInOutput?: boolean; } -export const useField = ( - form: FormHook, +export const useField = ( + form: FormHook, path: string, - config: FieldConfig & InternalFieldConfig = {}, - valueChangeListener?: (value: T) => void + config: FieldConfig & InternalFieldConfig = {}, + valueChangeListener?: (value: I) => void ) => { const { type = FIELD_TYPES.TEXT, @@ -44,7 +51,7 @@ export const useField = ( validations, formatters, fieldsToValidateOnChange, - errorDisplayDelay = form.__options.errorDisplayDelay, + valueChangeDebounceTime = form.__options.valueChangeDebounceTime, serializer, deserializer, } = config; @@ -68,7 +75,7 @@ export const useField = ( [initialValue, deserializer] ); - const [value, setStateValue] = useState(deserializeValue); + const [value, setStateValue] = useState(deserializeValue); const [errors, setErrors] = useState([]); const [isPristine, setPristine] = useState(true); const [isValidating, setValidating] = useState(false); @@ -84,9 +91,9 @@ export const useField = ( // -- HELPERS // ---------------------------------- - const serializeValue: FieldHook['__serializeValue'] = useCallback( - (rawValue = value) => { - return serializer ? serializer(rawValue) : rawValue; + const serializeValue: FieldHook['__serializeValue'] = useCallback( + (internalValue: I = value) => { + return serializer ? serializer(internalValue) : ((internalValue as unknown) as T); }, [serializer, value] ); @@ -129,16 +136,8 @@ export const useField = ( const changeIteration = ++changeCounter.current; const startTime = Date.now(); - if (debounceTimeout.current) { - clearTimeout(debounceTimeout.current); - debounceTimeout.current = null; - } - setPristine(false); - - if (errorDisplayDelay > 0) { - setIsChangingValue(true); - } + setIsChangingValue(true); // Notify listener if (valueChangeListener) { @@ -161,22 +160,24 @@ export const useField = ( * and then, we verify how long we've already waited for as form.__validateFields() is asynchronous * and might already have taken more than the specified delay) */ - if (errorDisplayDelay > 0 && changeIteration === changeCounter.current) { - const delta = Date.now() - startTime; - if (delta < errorDisplayDelay) { - debounceTimeout.current = setTimeout(() => { - debounceTimeout.current = null; - setIsChangingValue(false); - }, errorDisplayDelay - delta); - } else { - setIsChangingValue(false); + if (changeIteration === changeCounter.current) { + if (valueChangeDebounceTime > 0) { + const delta = Date.now() - startTime; + if (delta < valueChangeDebounceTime) { + debounceTimeout.current = setTimeout(() => { + debounceTimeout.current = null; + setIsChangingValue(false); + }, valueChangeDebounceTime - delta); + return; + } } + setIsChangingValue(false); } }, [ path, value, valueChangeListener, - errorDisplayDelay, + valueChangeDebounceTime, fieldsToValidateOnChange, __updateFormDataAt, __validateFields, @@ -207,7 +208,7 @@ export const useField = ( validationTypeToValidate, }: { formData: any; - value: T; + value: I; validationTypeToValidate?: string; }): ValidationError[] | Promise => { if (!validations) { @@ -339,7 +340,7 @@ export const useField = ( * If a validationType is provided then only that validation will be executed, * skipping the other type of validation that might exist. */ - const validate: FieldHook['validate'] = useCallback( + const validate: FieldHook['validate'] = useCallback( (validationData = {}) => { const { formData = getFormData({ unflatten: false }), @@ -392,14 +393,14 @@ export const useField = ( * * @param newValue The new value to assign to the field */ - const setValue: FieldHook['setValue'] = useCallback( + const setValue: FieldHook['setValue'] = useCallback( (newValue) => { setStateValue((prev) => { - let formattedValue: T; + let formattedValue: I; if (typeof newValue === 'function') { - formattedValue = formatInputValue((newValue as Function)(prev)); + formattedValue = formatInputValue((newValue as Function)(prev)); } else { - formattedValue = formatInputValue(newValue); + formattedValue = formatInputValue(newValue); } return formattedValue; }); @@ -407,7 +408,7 @@ export const useField = ( [formatInputValue] ); - const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { + const _setErrors: FieldHook['setErrors'] = useCallback((_errors) => { setErrors( _errors.map((error) => ({ validationType: VALIDATION_TYPES.FIELD, @@ -422,13 +423,13 @@ export const useField = ( * * @param event Form input change event */ - const onChange: FieldHook['onChange'] = useCallback( + const onChange: FieldHook['onChange'] = useCallback( (event) => { const newValue = {}.hasOwnProperty.call(event!.target, 'checked') ? event.target.checked : event.target.value; - setValue((newValue as unknown) as T); + setValue((newValue as unknown) as I); }, [setValue] ); @@ -443,7 +444,7 @@ export const useField = ( * * @param validationType The validation type to return error messages from */ - const getErrorsMessages: FieldHook['getErrorsMessages'] = useCallback( + const getErrorsMessages: FieldHook['getErrorsMessages'] = useCallback( (args = {}) => { const { errorCode, validationType = VALIDATION_TYPES.FIELD } = args; const errorMessages = errors.reduce((messages, error) => { @@ -464,30 +465,64 @@ export const useField = ( [errors] ); - const reset: FieldHook['reset'] = useCallback( + /** + * Handler to update the state and make sure the component is still mounted. + * When resetting the form, some field might get unmounted (e.g. a toggle on "true" becomes "false" and now certain fields should not be in the DOM). + * In that scenario there is a race condition in the "reset" method below, because the useState() hook is not synchronous. + * + * A better approach would be to have the state in a reducer and being able to update all values in a single dispatch action. + */ + const updateStateIfMounted = useCallback( + ( + state: 'isPristine' | 'isValidating' | 'isChangingValue' | 'isValidated' | 'errors' | 'value', + nextValue: any + ) => { + if (isMounted.current === false) { + return; + } + + switch (state) { + case 'value': + return setValue(nextValue); + case 'errors': + return setErrors(nextValue); + case 'isChangingValue': + return setIsChangingValue(nextValue); + case 'isPristine': + return setPristine(nextValue); + case 'isValidated': + return setIsValidated(nextValue); + case 'isValidating': + return setValidating(nextValue); + } + }, + [setValue] + ); + + const reset: FieldHook['reset'] = useCallback( (resetOptions = { resetValue: true }) => { const { resetValue = true, defaultValue: updatedDefaultValue } = resetOptions; - setPristine(true); - setValidating(false); - setIsChangingValue(false); - setIsValidated(false); - setErrors([]); + updateStateIfMounted('isPristine', true); + updateStateIfMounted('isValidating', false); + updateStateIfMounted('isChangingValue', false); + updateStateIfMounted('isValidated', false); + updateStateIfMounted('errors', []); if (resetValue) { hasBeenReset.current = true; const newValue = deserializeValue(updatedDefaultValue ?? defaultValue); - setValue(newValue); + updateStateIfMounted('value', newValue); return newValue; } }, - [setValue, deserializeValue, defaultValue] + [updateStateIfMounted, deserializeValue, defaultValue] ); // Don't take into account non blocker validation. Some are just warning (like trying to add a wrong ComboBox item) const isValid = errors.filter((e) => e.__isBlocking__ !== false).length === 0; - const field = useMemo>(() => { + const field = useMemo>(() => { return { path, type, @@ -565,6 +600,7 @@ export const useField = ( return () => { if (debounceTimeout.current) { clearTimeout(debounceTimeout.current); + debounceTimeout.current = null; } }; }, [onValueChange]); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx index edcd84daf5d2f..b28c09d07fa98 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.test.tsx @@ -196,7 +196,9 @@ describe('useForm() hook', () => { }); expect(isValid).toBe(false); - expect(data).toEqual({}); // Don't build the object (and call the serializers()) when invalid + // If the form is not valid, we don't build the final object to avoid + // calling the serializer(s) with invalid values. + expect(data).toEqual({}); }); }); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index b390c17d3c2ff..be4535fec3669 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -24,19 +24,18 @@ import { set } from '@elastic/safer-lodash-set'; import { FormHook, FieldHook, FormData, FieldConfig, FieldsMap, FormConfig } from '../types'; import { mapFormFields, unflattenObject, Subject, Subscription } from '../lib'; -const DEFAULT_ERROR_DISPLAY_TIMEOUT = 500; const DEFAULT_OPTIONS = { - errorDisplayDelay: DEFAULT_ERROR_DISPLAY_TIMEOUT, + valueChangeDebounceTime: 500, stripEmptyFields: true, }; -interface UseFormReturn { - form: FormHook; +interface UseFormReturn { + form: FormHook; } -export function useForm( - formConfig?: FormConfig -): UseFormReturn { +export function useForm( + formConfig?: FormConfig +): UseFormReturn { const { onSubmit, schema, serializer, deserializer, options, id = 'default', defaultValue } = formConfig ?? {}; @@ -48,9 +47,9 @@ export function useForm( const filtered = Object.entries(_defaultValue as object) .filter(({ 1: value }) => value !== undefined) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); + .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {} as T); - return deserializer ? (deserializer(filtered) as any) : filtered; + return deserializer ? deserializer(filtered) : filtered; }, [deserializer] ); @@ -61,13 +60,13 @@ export function useForm( const defaultValueDeserialized = useRef(defaultValueMemoized); - const { errorDisplayDelay, stripEmptyFields: doStripEmptyFields } = options ?? {}; + const { valueChangeDebounceTime, stripEmptyFields: doStripEmptyFields } = options ?? {}; const formOptions = useMemo( () => ({ stripEmptyFields: doStripEmptyFields ?? DEFAULT_OPTIONS.stripEmptyFields, - errorDisplayDelay: errorDisplayDelay ?? DEFAULT_OPTIONS.errorDisplayDelay, + valueChangeDebounceTime: valueChangeDebounceTime ?? DEFAULT_OPTIONS.valueChangeDebounceTime, }), - [errorDisplayDelay, doStripEmptyFields] + [valueChangeDebounceTime, doStripEmptyFields] ); const [isSubmitted, setIsSubmitted] = useState(false); @@ -93,7 +92,7 @@ export function useForm( return formData$.current; }, []); - const fieldsToArray = useCallback(() => Object.values(fieldsRefs.current), []); + const fieldsToArray = useCallback<() => FieldHook[]>(() => Object.values(fieldsRefs.current), []); const getFieldsForOutput = useCallback( (fields: FieldsMap, opts: { stripEmptyFields: boolean }): FieldsMap => { @@ -144,7 +143,7 @@ export function useForm( }); const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); return serializer - ? (serializer(unflattenObject(fieldsValue)) as T) + ? (serializer(unflattenObject(fieldsValue) as I) as T) : (unflattenObject(fieldsValue) as T); } @@ -175,6 +174,24 @@ export function useForm( const isFieldValid = (field: FieldHook) => field.isValid && !field.isValidating; + const waitForFieldsToFinishValidating = useCallback(async () => { + let areSomeFieldValidating = fieldsToArray().some((field) => field.isValidating); + if (!areSomeFieldValidating) { + return; + } + + return new Promise((resolve) => { + setTimeout(() => { + areSomeFieldValidating = fieldsToArray().some((field) => field.isValidating); + if (areSomeFieldValidating) { + // Recursively wait for all the fields to finish validating. + return waitForFieldsToFinishValidating().then(resolve); + } + resolve(); + }, 100); + }); + }, [fieldsToArray]); + const validateFields: FormHook['__validateFields'] = useCallback( async (fieldNames) => { const fieldsToValidate = fieldNames @@ -204,18 +221,25 @@ export function useForm( // To know the current form validity, we will then merge the "validationResult" _with_ the fieldsRefs object state, // the "validationResult" taking presedence over the fieldsRefs values. const formFieldsValidity = fieldsToArray().map((field) => { + const hasUpdatedValidity = validationResultByPath[field.path] !== undefined; const _isValid = validationResultByPath[field.path] ?? field.isValid; - const _isValidated = - validationResultByPath[field.path] !== undefined ? true : field.isValidated; - return [_isValid, _isValidated]; + const _isValidated = hasUpdatedValidity ? true : field.isValidated; + const _isValidating = hasUpdatedValidity ? false : field.isValidating; + return { + isValid: _isValid, + isValidated: _isValidated, + isValidating: _isValidating, + }; }); - const areAllFieldsValidated = formFieldsValidity.every(({ 1: isValidated }) => isValidated); + const areAllFieldsValidated = formFieldsValidity.every((field) => field.isValidated); + const areSomeFieldValidating = formFieldsValidity.some((field) => field.isValidating); // If *not* all the fiels have been validated, the validity of the form is unknown, thus still "undefined" - const isFormValid = areAllFieldsValidated - ? formFieldsValidity.every(([_isValid]) => _isValid) - : undefined; + const isFormValid = + areAllFieldsValidated && areSomeFieldValidating === false + ? formFieldsValidity.every((field) => field.isValid) + : undefined; setIsValid(isFormValid); @@ -225,6 +249,14 @@ export function useForm( ); const validateAllFields = useCallback(async (): Promise => { + // Maybe some field are being validated because of their async validation(s). + // We make sure those validations have finished executing before proceeding. + await waitForFieldsToFinishValidating(); + + if (!isMounted.current) { + return false; + } + const fieldsArray = fieldsToArray(); const fieldsToValidate = fieldsArray.filter((field) => !field.isValidated); @@ -238,7 +270,7 @@ export function useForm( setIsValid(isFormValid); return isFormValid!; - }, [fieldsToArray, validateFields]); + }, [fieldsToArray, validateFields, waitForFieldsToFinishValidating]); const addField: FormHook['__addField'] = useCallback( (field) => { @@ -303,7 +335,7 @@ export function useForm( const getFields: FormHook['getFields'] = useCallback(() => fieldsRefs.current, []); - const getFieldDefaultValue: FormHook['getFieldDefaultValue'] = useCallback( + const getFieldDefaultValue: FormHook['__getFieldDefaultValue'] = useCallback( (fieldName) => get(defaultValueDeserialized.current, fieldName), [] ); @@ -410,13 +442,13 @@ export function useForm( getFields, getFormData, getErrors, - getFieldDefaultValue, reset, __options: formOptions, __getFormData$: getFormData$, __updateFormDataAt: updateFormDataAt, __updateDefaultValueAt: updateDefaultValueAt, __readFieldConfigFromSchema: readFieldConfigFromSchema, + __getFieldDefaultValue: getFieldDefaultValue, __addField: addField, __removeField: removeField, __validateFields: validateFields, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts index fb4a0984438ad..6c6dee3624979 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data.ts @@ -63,7 +63,11 @@ export const useFormData = (options: Options = {}): ? (watch as string[]) : ([watch] as string[]); - if (valuesToWatchArray.some((value) => previousRawData.current[value] !== raw[value])) { + if ( + valuesToWatchArray.some( + (value) => previousRawData.current[value] !== raw[value as keyof T] + ) + ) { previousRawData.current = raw; // Only update the state if one of the field we watch has changed. setFormData(raw); diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 18b8f478f7c0e..ae731caff2881 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -24,21 +24,37 @@ import { Subject, Subscription } from './lib'; // Comes from https://github.com/microsoft/TypeScript/issues/15012#issuecomment-365453623 type Required = T extends FormData ? { [P in keyof T]-?: NonNullable } : T; -export interface FormHook { +export interface FormHook { + /** Flag that indicates if the form has been submitted at least once. It is set to `true` when we call `submit()`. */ readonly isSubmitted: boolean; + /** Flag that indicates if the form is being submitted. */ readonly isSubmitting: boolean; + /** Flag that indicates if the form is valid. If `undefined` then the form validation has not been checked yet. */ readonly isValid: boolean | undefined; + /** The form id. If none was provided, "default" will be returned. */ readonly id: string; + /** + * This handler submits the form and returns its data and validity. If the form is not valid, the data will be `null` + * as only valid data is passed through the `serializer(s)` before being returned. + */ submit: (e?: FormEvent | MouseEvent) => Promise<{ data: T; isValid: boolean }>; + /** Use this handler to get the validity of the form. */ validate: () => Promise; subscribe: (handler: OnUpdateHandler) => Subscription; + /** Sets a field value imperatively. */ setFieldValue: (fieldName: string, value: FieldValue) => void; + /** Sets a field errors imperatively. */ setFieldErrors: (fieldName: string, errors: ValidationError[]) => void; + /** Access any field on the form. */ getFields: () => FieldsMap; + /** + * Return the form data. It accepts an optional options object with an `unflatten` parameter (defaults to `true`). + * If you are only interested in the raw form data, pass `unflatten: false` to the handler + */ getFormData: (options?: { unflatten?: boolean }) => T; - getFieldDefaultValue: (fieldName: string) => unknown; - /* Returns a list of all errors in the form */ + /* Returns an array with of all errors in the form. */ getErrors: () => string[]; + /** Resets the form to its initial state. */ reset: (options?: { resetValues?: boolean; defaultValue?: Partial }) => void; readonly __options: Required; __getFormData$: () => Subject; @@ -50,23 +66,19 @@ export interface FormHook { __updateFormDataAt: (field: string, value: unknown) => T; __updateDefaultValueAt: (field: string, value: unknown) => void; __readFieldConfigFromSchema: (fieldName: string) => FieldConfig; + __getFieldDefaultValue: (fieldName: string) => unknown; } -export interface FormSchema { - [key: string]: FormSchemaEntry; -} - -type FormSchemaEntry = - | FieldConfig - | Array> - | { [key: string]: FieldConfig | Array> | FormSchemaEntry }; +export type FormSchema = { + [K in keyof T]?: FieldConfig | FormSchema; +}; -export interface FormConfig { +export interface FormConfig { onSubmit?: FormSubmitHandler; - schema?: FormSchema; + schema?: FormSchema; defaultValue?: Partial; - serializer?: SerializerFunc; - deserializer?: SerializerFunc; + serializer?: SerializerFunc; + deserializer?: SerializerFunc; options?: FormOptions; id?: string; } @@ -83,20 +95,20 @@ export interface OnFormUpdateArg { export type OnUpdateHandler = (arg: OnFormUpdateArg) => void; export interface FormOptions { - errorDisplayDelay?: number; + valueChangeDebounceTime?: number; /** * Remove empty string field ("") from form data */ stripEmptyFields?: boolean; } -export interface FieldHook { +export interface FieldHook { readonly path: string; readonly label?: string; readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; readonly type: string; - readonly value: T; + readonly value: I; readonly errors: ValidationError[]; readonly isValid: boolean; readonly isPristine: boolean; @@ -108,34 +120,33 @@ export interface FieldHook { errorCode?: string; }) => string | null; onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; - setValue: (value: T | ((prevValue: T) => T)) => void; + setValue: (value: I | ((prevValue: I) => I)) => void; setErrors: (errors: ValidationError[]) => void; clearErrors: (type?: string | string[]) => void; validate: (validateData?: { formData?: any; - value?: T; + value?: I; validationType?: string; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; // Flag to indicate if the field value will be included in the form data outputted // when calling form.getFormData(); __isIncludedInOutput: boolean; - __serializeValue: (rawValue?: unknown) => unknown; + __serializeValue: (internalValue?: I) => T; } -export interface FieldConfig { - readonly path?: string; +export interface FieldConfig { readonly label?: string; readonly labelAppend?: string | ReactNode; readonly helpText?: string | ReactNode; - readonly type?: HTMLInputElement['type']; - readonly defaultValue?: ValueType; - readonly validations?: Array>; + readonly type?: string; + readonly defaultValue?: T; + readonly validations?: Array>; readonly formatters?: FormatterFunc[]; - readonly deserializer?: SerializerFunc; - readonly serializer?: SerializerFunc; + readonly deserializer?: SerializerFunc; + readonly serializer?: SerializerFunc; readonly fieldsToValidateOnChange?: string[]; - readonly errorDisplayDelay?: number; + readonly valueChangeDebounceTime?: number; } export interface FieldsMap { @@ -166,7 +177,7 @@ export interface ValidationFuncArg { errors: readonly ValidationError[]; } -export type ValidationFunc = ( +export type ValidationFunc = ( data: ValidationFuncArg ) => ValidationError | void | undefined | Promise | void | undefined>; @@ -187,8 +198,12 @@ type FormatterFunc = (value: any, formData: FormData) => unknown; // string | number | boolean | string[] ... type FieldValue = unknown; -export interface ValidationConfig { - validator: ValidationFunc; +export interface ValidationConfig< + FormType extends FormData = any, + Error extends string = string, + ValueType = unknown +> { + validator: ValidationFunc; type?: string; /** * By default all validation are blockers, which means that if they fail, the field is invalid. diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap index bf1e8c8f0b401..e9b0494105e12 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.js.snap @@ -2,57 +2,26 @@ exports[`home change home route should render a link to change the default route in advanced settings if advanced settings is enabled 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -73,115 +42,36 @@ exports[`home change home route should render a link to change the default route aria-hidden="true" margin="xl" /> -
- - - - - - - - - - - - -
+
`; exports[`home directories should not render directory entry when showOnHomePage is false 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - - Manage - - - - -
-
-
+ + } + />
- 0 @@ -202,92 +92,36 @@ exports[`home directories should not render directory entry when showOnHomePage aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home directories should render ADMIN directory entry in "Manage your data" panel 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -320,92 +154,36 @@ exports[`home directories should render ADMIN directory entry in "Manage your da aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home directories should render DATA directory entry in "Ingest your data" panel 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -438,97 +216,43 @@ exports[`home directories should render DATA directory entry in "Ingest your dat aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home directories should render solutions in the "solution section" 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
`; exports[`home header render 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -700,102 +368,36 @@ exports[`home header render 1`] = ` aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home header should show "Dev tools" link if console is available 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - - Dev tools - - - - -
-
-
+ + } + />
- 0 @@ -828,103 +430,36 @@ exports[`home header should show "Dev tools" link if console is available 1`] = aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home header should show "Manage" link if stack management is available 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - - Manage - - - - -
-
-
+ + } + />
- 0 @@ -945,92 +480,36 @@ exports[`home header should show "Manage" link if stack management is available aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home isNewKibanaInstance should safely handle execeptions 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1051,92 +530,36 @@ exports[`home isNewKibanaInstance should safely handle execeptions 1`] = ` aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when there are index patterns 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1157,92 +580,36 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to false when t aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when there are no index patterns 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1263,92 +630,36 @@ exports[`home isNewKibanaInstance should set isNewKibanaInstance to true when th aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home should render home component 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1369,92 +680,36 @@ exports[`home should render home component 1`] = ` aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home welcome should show the normal home page if loading fails 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1475,92 +730,36 @@ exports[`home welcome should show the normal home page if loading fails 1`] = ` aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; exports[`home welcome should show the normal home page if welcome screen is disabled locally 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1581,35 +780,10 @@ exports[`home welcome should show the normal home page if welcome screen is disa aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; @@ -1623,57 +797,26 @@ exports[`home welcome should show the welcome screen if enabled, and there are n exports[`home welcome stores skip welcome setting if skipped 1`] = `
-
-
- - - -

- -

-
-
- - - - - Add data - - - - -
-
-
+ + } + />
- 0 @@ -1694,35 +837,10 @@ exports[`home welcome stores skip welcome setting if skipped 1`] = ` aria-hidden="true" margin="xl" /> -
- - - - - - - - -
+
`; diff --git a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap index 190985f70659d..ec192ce1eb32f 100644 --- a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap @@ -9,6 +9,7 @@ exports[`props iconType 1`] = ` href="link_to_item" icon={ - - - +
+ + + +
= ({ addBasePath, features }) => (
- +

@@ -43,18 +43,21 @@ export const AddData: FC = ({ addBasePath, features }) => ( - - - - + +
+ + + +
diff --git a/src/plugins/home/public/application/components/home.js b/src/plugins/home/public/application/components/home.js index becafb2560217..054f5a5344df4 100644 --- a/src/plugins/home/public/application/components/home.js +++ b/src/plugins/home/public/application/components/home.js @@ -20,18 +20,16 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { + OverviewPageFooter, + OverviewPageHeader, +} from '../../../../../../src/plugins/kibana_react/public'; +import { HOME_APP_BASE_PATH } from '../../../common/constants'; import { FeatureCatalogueCategory } from '../../services'; import { getServices } from '../kibana_services'; import { AddData } from './add_data'; -import { createAppNavigationHandler } from './app_navigation_handler'; import { ManageData } from './manage_data'; import { SolutionsSection } from './solutions_section'; import { Welcome } from './welcome'; @@ -121,12 +119,9 @@ export class Home extends Component { .sort((directoryA, directoryB) => directoryA.order - directoryB.order); renderNormal() { - const { addBasePath, solutions } = this.props; + const { addBasePath, solutions, directories } = this.props; const devTools = this.findDirectoryById('console'); - const stackManagement = this.findDirectoryById('stack-management'); - const advancedSettings = this.findDirectoryById('advanced_settings'); - const addDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.DATA); const manageDataFeatures = this.getFeaturesByCategory(FeatureCatalogueCategory.ADMIN); @@ -136,68 +131,27 @@ export class Home extends Component { } return ( -
-
-
- - - -

- -

-
-
- - - - - - {i18n.translate('home.pageHeader.addDataButtonLabel', { - defaultMessage: 'Add data', - })} - - - - {stackManagement ? ( - - - {i18n.translate('home.pageHeader.stackManagementButtonLabel', { - defaultMessage: 'Manage', - })} - - - ) : null} - - {devTools ? ( - - - {i18n.translate('home.pageHeader.devToolsButtonLabel', { - defaultMessage: 'Dev tools', - })} - - - ) : null} - - -
-
-
+
+ } + />
- {solutions.length && } + {solutions.length ? ( + + ) : null}
); @@ -294,12 +214,14 @@ Home.propTypes = { PropTypes.shape({ id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, + subtitle: PropTypes.string, description: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, path: PropTypes.string.isRequired, showOnHomePage: PropTypes.bool.isRequired, category: PropTypes.string.isRequired, order: PropTypes.number, + solutionId: PropTypes.string, }) ), solutions: PropTypes.arrayOf( @@ -307,7 +229,8 @@ Home.propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, subtitle: PropTypes.string.isRequired, - descriptions: PropTypes.arrayOf(PropTypes.string).isRequired, + description: PropTypes.string, + appDescriptions: PropTypes.arrayOf(PropTypes.string).isRequired, icon: PropTypes.string.isRequired, path: PropTypes.string.isRequired, order: PropTypes.number, diff --git a/src/plugins/home/public/application/components/home.test.js b/src/plugins/home/public/application/components/home.test.js index 0d7596d92a5a1..9c73bbf9b75ba 100644 --- a/src/plugins/home/public/application/components/home.test.js +++ b/src/plugins/home/public/application/components/home.test.js @@ -35,6 +35,11 @@ jest.mock('../kibana_services', () => ({ }), })); +jest.mock('../../../../../../src/plugins/kibana_react/public', () => ({ + OverviewPageFooter: jest.fn().mockReturnValue(<>), + OverviewPageHeader: jest.fn().mockReturnValue(<>), +})); + describe('home', () => { let defaultProps; @@ -142,7 +147,7 @@ describe('home', () => { id: 'kibana', title: 'Kibana', subtitle: 'Visualize & analyze', - descriptions: ['Analyze data in dashboards'], + appDescriptions: ['Analyze data in dashboards'], icon: 'logoKibana', path: 'kibana_landing_page', order: 1, @@ -151,7 +156,7 @@ describe('home', () => { id: 'solution-2', title: 'Solution two', subtitle: 'Subtitle for solution two', - descriptions: ['Example use case'], + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-two', order: 2, @@ -160,7 +165,7 @@ describe('home', () => { id: 'solution-3', title: 'Solution three', subtitle: 'Subtitle for solution three', - descriptions: ['Example use case'], + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-three', order: 3, @@ -169,7 +174,7 @@ describe('home', () => { id: 'solution-4', title: 'Solution four', subtitle: 'Subtitle for solution four', - descriptions: ['Example use case'], + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-four', order: 4, diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 69cd68d553d03..7fe4f4351c35b 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -104,12 +104,14 @@ HomeApp.propTypes = { PropTypes.shape({ id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, + subtitle: PropTypes.string, description: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, path: PropTypes.string.isRequired, showOnHomePage: PropTypes.bool.isRequired, category: PropTypes.string.isRequired, order: PropTypes.number, + solutionId: PropTypes.string, }) ), solutions: PropTypes.arrayOf( @@ -117,7 +119,8 @@ HomeApp.propTypes = { id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, subtitle: PropTypes.string.isRequired, - descriptions: PropTypes.arrayOf(PropTypes.string).isRequired, + description: PropTypes.string, + appDescriptions: PropTypes.arrayOf(PropTypes.string).isRequired, icon: PropTypes.string.isRequired, path: PropTypes.string.isRequired, order: PropTypes.number, diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx index 0e86bf7dd3d84..18a58e86eaa3f 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.test.tsx @@ -37,7 +37,6 @@ const mockFeatures = [ { category: 'admin', description: 'Control who has access and what tasks they can perform.', - homePageSection: 'manage_data', icon: 'securityApp', id: 'security', order: 600, @@ -48,7 +47,6 @@ const mockFeatures = [ { category: 'admin', description: 'Track the real-time health and performance of your deployment.', - homePageSection: 'manage_data', icon: 'monitoringApp', id: 'monitoring', order: 610, @@ -60,7 +58,6 @@ const mockFeatures = [ category: 'admin', description: 'Save snapshots to a backup repository, and restore to recover index and cluster state.', - homePageSection: 'manage_data', icon: 'storage', id: 'snapshot_restore', order: 630, @@ -71,7 +68,6 @@ const mockFeatures = [ { category: 'admin', description: 'Define lifecycle policies to automatically perform operations as an index ages.', - homePageSection: 'manage_data', icon: 'indexSettings', id: 'index_lifecycle_management', order: 640, diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap index ad92aac67d51b..726d3dda4e9cc 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solution_panel.test.tsx.snap @@ -13,6 +13,7 @@ exports[`SolutionPanel renders the solution panel for the given solution 1`] = ` onClick={[Function]} >

diff --git a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap index 7015ebb40a71d..4052ffca9e56f 100644 --- a/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap +++ b/src/plugins/home/public/application/components/solutions_section/__snapshots__/solutions_section.test.tsx.snap @@ -7,19 +7,15 @@ exports[`SolutionsSection only renders a spacer if no solutions are available 1` className="homSolutions" > - -

- -

-
+ +

- -

- -

-
+ +

- -

- -

-
+ +
- -

- -

-
+ +
- descriptions.map(getDescriptionText).reduce(addSpacersBetweenElementsReducer, []); +const getDescriptions = (appDescriptions: string[]) => + appDescriptions + .map(getDescriptionText) + .reduce(addSpacersBetweenElementsReducer, []); interface Props { addBasePath: (path: string) => string; solution: FeatureCatalogueSolution; + apps?: FeatureCatalogueEntry[]; } -export const SolutionPanel: FC = ({ addBasePath, solution }) => ( +export const SolutionPanel: FC = ({ addBasePath, solution, apps = [] }) => ( = ({ addBasePath, solution }) => ( href={addBasePath(solution.path)} onClick={createAppNavigationHandler(solution.path)} > - + = ({ addBasePath, solution }) => ( - {getDescriptions(solution.descriptions)} + {getDescriptions( + apps.length ? apps.map(({ subtitle = '' }) => subtitle) : solution.appDescriptions + )} diff --git a/src/plugins/home/public/application/components/solutions_section/solution_title.tsx b/src/plugins/home/public/application/components/solutions_section/solution_title.tsx index fb833a40807d2..a9874ff7ddbe7 100644 --- a/src/plugins/home/public/application/components/solutions_section/solution_title.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solution_title.tsx @@ -45,7 +45,7 @@ export const SolutionTitle: FC = ({ title, subtitle, iconType }) => ( className="homSolutionPanel__icon" /> - +

{title}

diff --git a/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx b/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx index 17d721cc96c02..9ec5bf695b35c 100644 --- a/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solutions_section.test.tsx @@ -20,12 +20,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SolutionsSection } from './solutions_section'; +import { FeatureCatalogueCategory } from '../../../services'; const solutionEntry1 = { id: 'kibana', title: 'Kibana', subtitle: 'Visualize & analyze', - descriptions: ['Analyze data in dashboards'], + appDescriptions: ['Analyze data in dashboards'], icon: 'logoKibana', path: 'kibana_landing_page', order: 1, @@ -34,7 +35,8 @@ const solutionEntry2 = { id: 'solution-2', title: 'Solution two', subtitle: 'Subtitle for solution two', - descriptions: ['Example use case'], + description: 'Description for solution two', + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-two', order: 2, @@ -43,7 +45,8 @@ const solutionEntry3 = { id: 'solution-3', title: 'Solution three', subtitle: 'Subtitle for solution three', - descriptions: ['Example use case'], + description: 'Description for solution three', + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-three', order: 3, @@ -52,23 +55,64 @@ const solutionEntry4 = { id: 'solution-4', title: 'Solution four', subtitle: 'Subtitle for solution four', - descriptions: ['Example use case'], + description: 'Description for solution four', + appDescriptions: ['Example use case'], icon: 'empty', path: 'path-to-solution-four', order: 4, }; +const mockDirectories = [ + { + id: 'dashboard', + title: 'Dashboard', + description: 'Description of dashboard', + icon: 'dashboardApp', + path: 'dashboard_landing_page', + showOnHomePage: false, + category: FeatureCatalogueCategory.DATA, + }, + { + id: 'discover', + title: 'Discover', + description: 'Description of discover', + icon: 'discoverApp', + path: 'discover_landing_page', + showOnHomePage: false, + category: FeatureCatalogueCategory.DATA, + }, + { + id: 'canvas', + title: 'Canvas', + description: 'Description of canvas', + icon: 'canvasApp', + path: 'canvas_landing_page', + showOnHomePage: false, + category: FeatureCatalogueCategory.DATA, + }, +]; + const addBasePathMock = (path: string) => (path ? path : 'path'); describe('SolutionsSection', () => { test('only renders a spacer if no solutions are available', () => { - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); test('renders a single solution', () => { const component = shallow( - + ); expect(component).toMatchSnapshot(); }); @@ -78,6 +122,7 @@ describe('SolutionsSection', () => { ); expect(component).toMatchSnapshot(); @@ -87,6 +132,7 @@ describe('SolutionsSection', () => { ); expect(component).toMatchSnapshot(); diff --git a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx index 1a78a6c71030a..13b70383147eb 100644 --- a/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx +++ b/src/plugins/home/public/application/components/solutions_section/solutions_section.tsx @@ -19,16 +19,10 @@ import React, { FC } from 'react'; import PropTypes from 'prop-types'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, - EuiScreenReaderOnly, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { SolutionPanel } from './solution_panel'; -import { FeatureCatalogueSolution } from '../../../'; +import { FeatureCatalogueEntry, FeatureCatalogueSolution } from '../../../'; const sortByOrder = ( { order: orderA = 0 }: FeatureCatalogueSolution, @@ -38,25 +32,25 @@ const sortByOrder = ( interface Props { addBasePath: (path: string) => string; solutions: FeatureCatalogueSolution[]; + directories: FeatureCatalogueEntry[]; } -export const SolutionsSection: FC = ({ addBasePath, solutions }) => { +export const SolutionsSection: FC = ({ addBasePath, solutions, directories }) => { // Separate Kibana from other solutions const kibana = solutions.find(({ id }) => id === 'kibana'); + const kibanaApps = directories.filter(({ solutionId }) => solutionId === 'kibana'); solutions = solutions.sort(sortByOrder).filter(({ id }) => id !== 'kibana'); return ( <>
- -

- -

-
+

+ +

@@ -69,7 +63,13 @@ export const SolutionsSection: FC = ({ addBasePath, solutions }) => { ) : null} - {kibana ? : null} + {kibana ? ( + + ) : null}
@@ -79,12 +79,28 @@ export const SolutionsSection: FC = ({ addBasePath, solutions }) => { }; SolutionsSection.propTypes = { + addBasePath: PropTypes.func.isRequired, + directories: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + subtitle: PropTypes.string, + description: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + showOnHomePage: PropTypes.bool.isRequired, + category: PropTypes.string.isRequired, + order: PropTypes.number, + solutionId: PropTypes.string, + }) + ), solutions: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, subtitle: PropTypes.string.isRequired, - descriptions: PropTypes.arrayOf(PropTypes.string).isRequired, + description: PropTypes.string, + appDescriptions: PropTypes.arrayOf(PropTypes.string).isRequired, icon: PropTypes.string.isRequired, path: PropTypes.string.isRequired, order: PropTypes.number, diff --git a/src/plugins/home/public/application/components/synopsis.js b/src/plugins/home/public/application/components/synopsis.js index fbe3bb3ed6769..0777c0db7210e 100644 --- a/src/plugins/home/public/application/components/synopsis.js +++ b/src/plugins/home/public/application/components/synopsis.js @@ -38,7 +38,7 @@ export function Synopsis({ if (iconUrl) { optionalImg = ; } else if (iconType) { - optionalImg = ; + optionalImg = ; } const classes = classNames('homSynopsis__card', { diff --git a/src/plugins/home/public/assets/auditd_logs/screenshot.png b/src/plugins/home/public/assets/auditd_logs/screenshot.png new file mode 100644 index 0000000000000..732afa18dc11c Binary files /dev/null and b/src/plugins/home/public/assets/auditd_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/coredns_logs/screenshot.png b/src/plugins/home/public/assets/coredns_logs/screenshot.png new file mode 100644 index 0000000000000..70921fa9bafb2 Binary files /dev/null and b/src/plugins/home/public/assets/coredns_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/crowdstrike_logs/screenshot.png b/src/plugins/home/public/assets/crowdstrike_logs/screenshot.png new file mode 100644 index 0000000000000..b74edfe2293f9 Binary files /dev/null and b/src/plugins/home/public/assets/crowdstrike_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/googlecloud_logs/screenshot.png b/src/plugins/home/public/assets/googlecloud_logs/screenshot.png new file mode 100644 index 0000000000000..4f68932e9f709 Binary files /dev/null and b/src/plugins/home/public/assets/googlecloud_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/haproxy_logs/screenshot.png b/src/plugins/home/public/assets/haproxy_logs/screenshot.png new file mode 100644 index 0000000000000..85a24bf01f3aa Binary files /dev/null and b/src/plugins/home/public/assets/haproxy_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/icinga_logs/screenshot.png b/src/plugins/home/public/assets/icinga_logs/screenshot.png new file mode 100644 index 0000000000000..013b20fcf166e Binary files /dev/null and b/src/plugins/home/public/assets/icinga_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/logos/barracuda.svg b/src/plugins/home/public/assets/logos/barracuda.svg new file mode 100644 index 0000000000000..555cdd6f8a32b --- /dev/null +++ b/src/plugins/home/public/assets/logos/barracuda.svg @@ -0,0 +1,100 @@ + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/checkpoint.svg b/src/plugins/home/public/assets/logos/checkpoint.svg new file mode 100644 index 0000000000000..e71866e78c293 --- /dev/null +++ b/src/plugins/home/public/assets/logos/checkpoint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/crowdstrike.svg b/src/plugins/home/public/assets/logos/crowdstrike.svg new file mode 100644 index 0000000000000..1b2195a2244f9 --- /dev/null +++ b/src/plugins/home/public/assets/logos/crowdstrike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/cylance.svg b/src/plugins/home/public/assets/logos/cylance.svg new file mode 100644 index 0000000000000..ccd6004d19e76 --- /dev/null +++ b/src/plugins/home/public/assets/logos/cylance.svg @@ -0,0 +1,82 @@ + + + + +Cylance_BB_Logo_RGB_Vert_Black + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/f5.svg b/src/plugins/home/public/assets/logos/f5.svg new file mode 100644 index 0000000000000..d985bde96291f --- /dev/null +++ b/src/plugins/home/public/assets/logos/f5.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/fortinet.svg b/src/plugins/home/public/assets/logos/fortinet.svg new file mode 100644 index 0000000000000..d6a8448f320bc --- /dev/null +++ b/src/plugins/home/public/assets/logos/fortinet.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/icinga.svg b/src/plugins/home/public/assets/logos/icinga.svg new file mode 100644 index 0000000000000..88161d61ca75e --- /dev/null +++ b/src/plugins/home/public/assets/logos/icinga.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/infoblox.svg b/src/plugins/home/public/assets/logos/infoblox.svg new file mode 100644 index 0000000000000..57b4d23b16812 --- /dev/null +++ b/src/plugins/home/public/assets/logos/infoblox.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/juniper.svg b/src/plugins/home/public/assets/logos/juniper.svg new file mode 100644 index 0000000000000..8802414a5aafe --- /dev/null +++ b/src/plugins/home/public/assets/logos/juniper.svg @@ -0,0 +1,72 @@ + +image/svg+xml \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/linux.svg b/src/plugins/home/public/assets/logos/linux.svg new file mode 100644 index 0000000000000..c0a92e0c0f404 --- /dev/null +++ b/src/plugins/home/public/assets/logos/linux.svg @@ -0,0 +1,1532 @@ + + + + Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Tux + 20 June 2012 + + + Garrett LeSage + + + + + + Larry Ewing, the creator of the original Tux graphic + + + + + tux + Linux + penguin + logo + + + + + Larry Ewing, Garrett LeSage + + + https://github.com/garrett/Tux + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/microsoft.svg b/src/plugins/home/public/assets/logos/microsoft.svg new file mode 100644 index 0000000000000..5334aa7ca6864 --- /dev/null +++ b/src/plugins/home/public/assets/logos/microsoft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/misp.svg b/src/plugins/home/public/assets/logos/misp.svg new file mode 100644 index 0000000000000..1cc61eda0b608 --- /dev/null +++ b/src/plugins/home/public/assets/logos/misp.svg @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + Threat Sharing + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/netscout.svg b/src/plugins/home/public/assets/logos/netscout.svg new file mode 100644 index 0000000000000..cbd25cd92594a --- /dev/null +++ b/src/plugins/home/public/assets/logos/netscout.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/plugins/home/public/assets/logos/o365.svg b/src/plugins/home/public/assets/logos/o365.svg new file mode 100644 index 0000000000000..3763f267ffc7a --- /dev/null +++ b/src/plugins/home/public/assets/logos/o365.svg @@ -0,0 +1,16 @@ + + + logo-integrations-Desktop HD Copy 2 + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/okta.svg b/src/plugins/home/public/assets/logos/okta.svg new file mode 100644 index 0000000000000..d806cb7dc6451 --- /dev/null +++ b/src/plugins/home/public/assets/logos/okta.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/oracle.svg b/src/plugins/home/public/assets/logos/oracle.svg new file mode 100644 index 0000000000000..1056521d9d5b3 --- /dev/null +++ b/src/plugins/home/public/assets/logos/oracle.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/osquery.svg b/src/plugins/home/public/assets/logos/osquery.svg new file mode 100755 index 0000000000000..c2bf733d35931 --- /dev/null +++ b/src/plugins/home/public/assets/logos/osquery.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/paloalto.svg b/src/plugins/home/public/assets/logos/paloalto.svg new file mode 100644 index 0000000000000..8c8e71ae0d9fc --- /dev/null +++ b/src/plugins/home/public/assets/logos/paloalto.svg @@ -0,0 +1,29 @@ + + + logo-integrations-Desktop HD + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/rabbitmq.svg b/src/plugins/home/public/assets/logos/rabbitmq.svg new file mode 100644 index 0000000000000..dabd2a5744cb4 --- /dev/null +++ b/src/plugins/home/public/assets/logos/rabbitmq.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/home/public/assets/logos/radware.svg b/src/plugins/home/public/assets/logos/radware.svg new file mode 100644 index 0000000000000..6252efef77624 --- /dev/null +++ b/src/plugins/home/public/assets/logos/radware.svg @@ -0,0 +1,66 @@ + +image/svg+xml \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/sonicwall.svg b/src/plugins/home/public/assets/logos/sonicwall.svg new file mode 100644 index 0000000000000..fb1aded68a29e --- /dev/null +++ b/src/plugins/home/public/assets/logos/sonicwall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/sophos.svg b/src/plugins/home/public/assets/logos/sophos.svg new file mode 100644 index 0000000000000..1f2cfc3a7f036 --- /dev/null +++ b/src/plugins/home/public/assets/logos/sophos.svg @@ -0,0 +1,69 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/src/plugins/home/public/assets/logos/tomcat.svg b/src/plugins/home/public/assets/logos/tomcat.svg new file mode 100644 index 0000000000000..410a468872e17 --- /dev/null +++ b/src/plugins/home/public/assets/logos/tomcat.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/home/public/assets/logos/zscaler.svg b/src/plugins/home/public/assets/logos/zscaler.svg new file mode 100644 index 0000000000000..b8a21a2fa6eed --- /dev/null +++ b/src/plugins/home/public/assets/logos/zscaler.svg @@ -0,0 +1 @@ +Zscaler-Logo-TM-Blue-RGB-May2019 \ No newline at end of file diff --git a/src/plugins/home/public/assets/microsoft_logs/screenshot.png b/src/plugins/home/public/assets/microsoft_logs/screenshot.png new file mode 100644 index 0000000000000..7df250e2ae885 Binary files /dev/null and b/src/plugins/home/public/assets/microsoft_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/misp_logs/screenshot.png b/src/plugins/home/public/assets/misp_logs/screenshot.png new file mode 100644 index 0000000000000..a02068ddf3038 Binary files /dev/null and b/src/plugins/home/public/assets/misp_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/mongodb_logs/screenshot.png b/src/plugins/home/public/assets/mongodb_logs/screenshot.png new file mode 100644 index 0000000000000..c77c37d5ce05b Binary files /dev/null and b/src/plugins/home/public/assets/mongodb_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/o365_logs/screenshot.png b/src/plugins/home/public/assets/o365_logs/screenshot.png new file mode 100644 index 0000000000000..a2413e7b909bc Binary files /dev/null and b/src/plugins/home/public/assets/o365_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/okta_logs/screenshot.png b/src/plugins/home/public/assets/okta_logs/screenshot.png new file mode 100644 index 0000000000000..6a28b4363b05b Binary files /dev/null and b/src/plugins/home/public/assets/okta_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/panw_logs/screenshot.png b/src/plugins/home/public/assets/panw_logs/screenshot.png new file mode 100644 index 0000000000000..ef9d5f706eca6 Binary files /dev/null and b/src/plugins/home/public/assets/panw_logs/screenshot.png differ diff --git a/src/plugins/home/public/assets/santa_logs/screenshot.png b/src/plugins/home/public/assets/santa_logs/screenshot.png new file mode 100644 index 0000000000000..31abdeb270a35 Binary files /dev/null and b/src/plugins/home/public/assets/santa_logs/screenshot.png differ diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index 4459d187eead7..e4ca6fb1439e8 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -24,6 +24,7 @@ export { EnvironmentSetup, TutorialSetup, HomePublicPluginSetup, + HomePublicPluginStart, } from './plugin'; export { FeatureCatalogueEntry, diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 7b56c6ec89b77..f8ff7c95aae08 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -52,23 +52,6 @@ describe('HomePublicPlugin', () => { ); }); - test('registers kibana solution to feature catalogue', async () => { - const setup = await new HomePublicPlugin(mockInitializerContext).setup( - coreMock.createSetup() as any, - { - urlForwarding: urlForwardingPluginMock.createSetupContract(), - } - ); - expect(setup).toHaveProperty('featureCatalogue'); - expect(setup.featureCatalogue.registerSolution).toHaveBeenCalledTimes(1); - expect(setup.featureCatalogue.registerSolution).toHaveBeenCalledWith( - expect.objectContaining({ - icon: 'logoKibana', - id: 'kibana', - }) - ); - }); - test('wires up and returns registry', async () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index b62ceae3d0d37..90f2f939101cb 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -58,7 +58,12 @@ export interface HomePluginSetupDependencies { export class HomePublicPlugin implements - Plugin { + Plugin< + HomePublicPluginSetup, + HomePublicPluginStart, + HomePluginSetupDependencies, + HomePluginStartDependencies + > { private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); private readonly environmentService = new EnvironmentService(); private readonly tutorialService = new TutorialService(); @@ -128,39 +133,6 @@ export class HomePublicPlugin order: 500, }); - featureCatalogue.registerSolution({ - id: 'kibana', - title: i18n.translate('home.kibana.featureCatalogue.title', { - defaultMessage: 'Kibana', - }), - subtitle: i18n.translate('home.kibana.featureCatalogue.subtitle', { - defaultMessage: 'Visualize & analyze', - }), - descriptions: [ - i18n.translate('home.kibana.featureCatalogueDescription1', { - defaultMessage: 'Analyze data in dashboards.', - }), - i18n.translate('home.kibana.featureCatalogueDescription2', { - defaultMessage: 'Search and find insights.', - }), - i18n.translate('home.kibana.featureCatalogueDescription3', { - defaultMessage: 'Design pixel-perfect reports.', - }), - i18n.translate('home.kibana.featureCatalogueDescription4', { - defaultMessage: 'Plot geographic data.', - }), - i18n.translate('home.kibana.featureCatalogueDescription5', { - defaultMessage: 'Model, predict, and detect.', - }), - i18n.translate('home.kibana.featureCatalogueDescription6', { - defaultMessage: 'Reveal patterns and relationships.', - }), - ], - icon: 'logoKibana', - path: '/app/dashboards', - order: 400, - }); - return { featureCatalogue, environment: { ...this.environmentService.setup() }, @@ -188,6 +160,8 @@ export class HomePublicPlugin } }); } + + return { featureCatalogue: this.featuresCatalogueRegistry }; } } @@ -212,3 +186,6 @@ export interface HomePublicPluginSetup { environment: EnvironmentSetup; } +export interface HomePublicPluginStart { + featureCatalogue: FeatureCatalogueRegistry; +} diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts index 23f36cef89ee6..e1a415ba2d571 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.mock.ts @@ -36,6 +36,7 @@ const createMock = (): jest.Mocked> => start: jest.fn(), get: jest.fn(() => []), getSolutions: jest.fn(() => []), + removeFeature: jest.fn(), }; service.setup.mockImplementation(createSetupMock); return service; diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts index b9902e0b10fb1..b009041bbf15b 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.test.ts @@ -38,7 +38,7 @@ const KIBANA_SOLUTION: FeatureCatalogueSolution = { id: 'kibana', title: 'Kibana', subtitle: 'Visualize & analyze', - descriptions: ['Analyze data in dashboards.', 'Search and find insights.'], + appDescriptions: ['Analyze data in dashboards.', 'Search and find insights.'], icon: 'kibanaApp', path: `/app/home`, }; diff --git a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts index d965042b65cef..845070da0db9f 100644 --- a/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts +++ b/src/plugins/home/public/services/feature_catalogue/feature_catalogue_registry.ts @@ -35,6 +35,8 @@ export interface FeatureCatalogueEntry { readonly title: string; /** {@link FeatureCatalogueCategory} to display this feature in. */ readonly category: FeatureCatalogueCategory; + /** A tagline of feature displayed to the user. */ + readonly subtitle?: string; /** One-line description of feature displayed to the user. */ readonly description: string; /** EUI `IconType` for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or ReactElement. */ @@ -47,6 +49,8 @@ export interface FeatureCatalogueEntry { readonly order?: number; /** Optional function to control visibility of this feature. */ readonly visible?: () => boolean; + /** Unique string identifier of the solution this feature belongs to */ + readonly solutionId?: string; } /** @public */ @@ -57,8 +61,10 @@ export interface FeatureCatalogueSolution { readonly title: string; /** The tagline of the solution displayed to the user. */ readonly subtitle: string; + /** One-line description of the solution displayed to the user. */ + readonly description?: string; /** A list of use cases for this solution displayed to the user. */ - readonly descriptions: string[]; + readonly appDescriptions: string[]; /** EUI `IconType` for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or ReactElement. */ readonly icon: IconType; /** URL path to link to this future. Should not include the basePath. */ @@ -99,7 +105,7 @@ export class FeatureCatalogueRegistry { this.capabilities = capabilities; } - public get(): readonly FeatureCatalogueEntry[] { + public get(): FeatureCatalogueEntry[] { if (this.capabilities === null) { throw new Error('Catalogue entries are only available after start phase'); } @@ -112,7 +118,7 @@ export class FeatureCatalogueRegistry { .sort(compareByKey('title')); } - public getSolutions(): readonly FeatureCatalogueSolution[] { + public getSolutions(): FeatureCatalogueSolution[] { if (this.capabilities === null) { throw new Error('Catalogue entries are only available after start phase'); } @@ -121,6 +127,10 @@ export class FeatureCatalogueRegistry { .filter((solution) => capabilities.catalogue[solution.id] !== false) .sort(compareByKey('title')); } + + public removeFeature(appId: string) { + this.features.delete(appId); + } } export type FeatureCatalogueRegistrySetup = ReturnType; diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts index c11c070637ae1..b0214f7a6422b 100644 --- a/src/plugins/home/server/tutorials/activemq_logs/index.ts +++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts @@ -53,11 +53,11 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche artifacts: { dashboards: [ { - id: '26434790-1464-11ea-8fd8-030a13064883', + id: 'ffe86390-145f-11ea-8fd8-030a13064883', linkLabel: i18n.translate('home.tutorials.activemqLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'ActiveMQ Application Events', + defaultMessage: 'ActiveMQ Audit Events', }), - isOverview: true, + isOverview: false, }, ], exportedFields: { diff --git a/src/plugins/home/server/tutorials/auditd_logs/index.ts b/src/plugins/home/server/tutorials/auditd_logs/index.ts new file mode 100644 index 0000000000000..365a7dcca44fe --- /dev/null +++ b/src/plugins/home/server/tutorials/auditd_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function auditdLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'auditd'; + const platforms = ['DEB', 'RPM'] as const; + return { + id: 'auditdLogs', + name: i18n.translate('home.tutorials.auditdLogs.nameTitle', { + defaultMessage: 'Auditd logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.auditdLogs.shortDescription', { + defaultMessage: 'Collect logs from the Linux auditd daemon.', + }), + longDescription: i18n.translate('home.tutorials.auditdLogs.longDescription', { + defaultMessage: + 'The module collects and parses logs from the audit daemon ( `auditd`). \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-auditd.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/linux.svg', + artifacts: { + dashboards: [ + { + id: 'dfbb49f0-0a0f-11e7-8a62-2d05eaaac5cb-ecs', + linkLabel: i18n.translate('home.tutorials.auditdLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Audit Events', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-auditd.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/auditd_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/barracuda_logs/index.ts b/src/plugins/home/server/tutorials/barracuda_logs/index.ts new file mode 100644 index 0000000000000..b5792b7535cc3 --- /dev/null +++ b/src/plugins/home/server/tutorials/barracuda_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function barracudaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'barracuda'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'barracudaLogs', + name: i18n.translate('home.tutorials.barracudaLogs.nameTitle', { + defaultMessage: 'Barracuda logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.barracudaLogs.shortDescription', { + defaultMessage: 'Collect Barracuda Web Application Firewall logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.barracudaLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Barracuda Web Application Firewall logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-barracuda.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/barracuda.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.barracudaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-barracuda.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/bluecoat_logs/index.ts b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts new file mode 100644 index 0000000000000..5e55b6b2c6455 --- /dev/null +++ b/src/plugins/home/server/tutorials/bluecoat_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function bluecoatLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'bluecoat'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'bluecoatLogs', + name: i18n.translate('home.tutorials.bluecoatLogs.nameTitle', { + defaultMessage: 'Bluecoat logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.bluecoatLogs.shortDescription', { + defaultMessage: 'Collect Blue Coat Director logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.bluecoatLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Blue Coat Director logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-bluecoat.html', + }, + }), + euiIconType: 'logoLogging', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.bluecoatLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-bluecoat.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/cef_logs/index.ts b/src/plugins/home/server/tutorials/cef_logs/index.ts new file mode 100644 index 0000000000000..6395f0ae76d5e --- /dev/null +++ b/src/plugins/home/server/tutorials/cef_logs/index.ts @@ -0,0 +1,78 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function cefLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'cef'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'cefLogs', + name: i18n.translate('home.tutorials.cefLogs.nameTitle', { + defaultMessage: 'CEF logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.cefLogs.shortDescription', { + defaultMessage: 'Collect Common Event Format (CEF) log data over syslog.', + }), + longDescription: i18n.translate('home.tutorials.cefLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Common Event Format (CEF) data over \ + Syslog. When messages are received over the syslog protocol the syslog \ + input will parse the header and set the timestamp value. Then the \ + processor is applied to parse the CEF encoded data. The decoded data \ + is written into a `cef` object field. Lastly any Elastic Common Schema \ + (ECS) fields that can be populated with the CEF data are populated. \ + [Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cef.html', + }, + }), + euiIconType: 'logoLogging', + artifacts: { + dashboards: [ + { + id: 'dd0bc9af-2e89-4150-9b42-62517ea56b71', + linkLabel: i18n.translate('home.tutorials.cefLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'CEF Network Overview Dashboard', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-cef.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/checkpoint_logs/index.ts b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts new file mode 100644 index 0000000000000..ed7051f63a32f --- /dev/null +++ b/src/plugins/home/server/tutorials/checkpoint_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function checkpointLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'checkpoint'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'checkpointLogs', + name: i18n.translate('home.tutorials.checkpointLogs.nameTitle', { + defaultMessage: 'Check Point logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.checkpointLogs.shortDescription', { + defaultMessage: 'Collect Check Point firewall logs.', + }), + longDescription: i18n.translate('home.tutorials.checkpointLogs.longDescription', { + defaultMessage: + 'This is a module for Check Point firewall logs. It supports logs from the Log Exporter in the Syslog format. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-checkpoint.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/checkpoint.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.checkpointLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-checkpoint.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts index b771744a069c3..b60a2137d680e 100644 --- a/src/plugins/home/server/tutorials/cisco_logs/index.ts +++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts @@ -35,17 +35,16 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema return { id: 'ciscoLogs', name: i18n.translate('home.tutorials.ciscoLogs.nameTitle', { - defaultMessage: 'Cisco', + defaultMessage: 'Cisco logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.ciscoLogs.shortDescription', { - defaultMessage: 'Collect and parse logs received from Cisco ASA firewalls.', + defaultMessage: 'Collect Cisco network device logs over syslog or from a file.', }), longDescription: i18n.translate('home.tutorials.ciscoLogs.longDescription', { defaultMessage: - 'This is a module for Cisco network device’s logs. Currently \ -supports the "asa" fileset for Cisco ASA firewall logs received over syslog or read from a file. \ + 'This is a module for Cisco network devices logs (ASA, FTD, IOS, Nexus). It includes the following filesets for receiving logs over syslog or read from a file: \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cisco.html', @@ -53,13 +52,15 @@ supports the "asa" fileset for Cisco ASA firewall logs received over syslog or r }), euiIconType: '/plugins/home/assets/logos/cisco.svg', artifacts: { - dashboards: [], - application: { - path: '/app/security', - label: i18n.translate('home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Security App', - }), - }, + dashboards: [ + { + id: 'a555b160-4987-11e9-b8ce-ed898b5ef295', + linkLabel: i18n.translate('home.tutorials.ciscoLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'ASA Firewall Dashboard', + }), + isOverview: true, + }, + ], exportedFields: { documentationUrl: '{config.docs.beats.filebeat}/exported-fields-cisco.html', }, diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts index 7fc8a2402d216..13467e628c7fb 100644 --- a/src/plugins/home/server/tutorials/coredns_logs/index.ts +++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts @@ -31,7 +31,7 @@ import { export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchema { const moduleName = 'coredns'; - const platforms = ['OSX', 'DEB', 'RPM'] as const; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; return { id: 'corednsLogs', name: i18n.translate('home.tutorials.corednsLogs.nameTitle', { @@ -40,12 +40,11 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.corednsLogs.shortDescription', { - defaultMessage: 'Collect the logs created by Coredns.', + defaultMessage: 'Collect CoreDNS logs.', }), longDescription: i18n.translate('home.tutorials.corednsLogs.longDescription', { defaultMessage: - 'The `coredns` Filebeat module collects the logs from \ -[CoreDNS](https://coredns.io/manual/toc/). \ + 'This is a filebeat module for CoreDNS. It supports both standalone CoreDNS deployment and CoreDNS deployment in Kubernetes. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-coredns.html', @@ -57,7 +56,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem { id: '53aa1f70-443e-11e9-8548-ab7fbe04f038', linkLabel: i18n.translate('home.tutorials.corednsLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'CoreDNS logs dashboard', + defaultMessage: '[Filebeat CoreDNS] Overview', }), isOverview: true, }, @@ -67,7 +66,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.jpg', + previewImagePath: '/plugins/home/assets/coredns_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts new file mode 100644 index 0000000000000..e4dd2bc6bb9c2 --- /dev/null +++ b/src/plugins/home/server/tutorials/crowdstrike_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function crowdstrikeLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'crowdstrike'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'crowdstrikeLogs', + name: i18n.translate('home.tutorials.crowdstrikeLogs.nameTitle', { + defaultMessage: 'CrowdStrike logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.crowdstrikeLogs.shortDescription', { + defaultMessage: 'Collect CrowdStrike Falcon logs using the Falcon SIEM Connector.', + }), + longDescription: i18n.translate('home.tutorials.crowdstrikeLogs.longDescription', { + defaultMessage: + 'This is the Filebeat module for CrowdStrike Falcon using the Falcon \ + [SIEM Connector](https://www.crowdstrike.com/blog/tech-center/integrate-with-your-siem). \ + This module collects this data, converts it to ECS, and ingests it to view in the SIEM. \ + By default, the Falcon SIEM connector outputs JSON formatted Falcon Streaming API event data. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-crowdstrike.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/crowdstrike.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.crowdstrikeLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-crowdstrike.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/cylance_logs/index.ts b/src/plugins/home/server/tutorials/cylance_logs/index.ts new file mode 100644 index 0000000000000..387a56febb83b --- /dev/null +++ b/src/plugins/home/server/tutorials/cylance_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function cylanceLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'cylance'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'cylanceLogs', + name: i18n.translate('home.tutorials.cylanceLogs.nameTitle', { + defaultMessage: 'CylancePROTECT logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.cylanceLogs.shortDescription', { + defaultMessage: 'Collect CylancePROTECT logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.cylanceLogs.longDescription', { + defaultMessage: + 'This is a module for receiving CylancePROTECT logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-cylance.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/cylance.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.cylanceLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-cylance.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts index f6c280d29f67f..b30457ea7848b 100644 --- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts +++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts @@ -65,6 +65,7 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria }, }, completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/elasticsearch_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts index 0cf032e6b90c1..94b5af143447f 100644 --- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts +++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts @@ -35,17 +35,16 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc return { id: 'envoyproxyLogs', name: i18n.translate('home.tutorials.envoyproxyLogs.nameTitle', { - defaultMessage: 'Envoyproxy', + defaultMessage: 'Envoy Proxy logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.envoyproxyLogs.shortDescription', { - defaultMessage: 'Collect and parse logs received from the Envoy proxy.', + defaultMessage: 'Collect Envoy Proxy logs.', }), longDescription: i18n.translate('home.tutorials.envoyproxyLogs.longDescription', { defaultMessage: - 'This is a filebeat module for [Envoy proxy access log](https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log). \ -It supports both standalone deployment and Envoy proxy deployment in Kubernetes. \ + 'This is a Filebeat module for Envoy proxy access log ( https://www.envoyproxy.io/docs/envoy/v1.10.0/configuration/access_log). It supports both standalone deployment and Envoy proxy deployment in Kubernetes. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-envoyproxy.html', @@ -53,13 +52,18 @@ It supports both standalone deployment and Envoy proxy deployment in Kubernetes. }), euiIconType: '/plugins/home/assets/logos/envoyproxy.svg', artifacts: { - dashboards: [], - application: { - path: '/app/security', - label: i18n.translate('home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Security App', - }), - }, + dashboards: [ + { + id: '0c610510-5cbd-11e9-8477-077ec9664dbd', + linkLabel: i18n.translate( + 'home.tutorials.envoyproxyLogs.artifacts.dashboards.linkLabel', + { + defaultMessage: 'Envoy Proxy Overview', + } + ), + isOverview: true, + }, + ], exportedFields: { documentationUrl: '{config.docs.beats.filebeat}/exported-fields-envoyproxy.html', }, diff --git a/src/plugins/home/server/tutorials/f5_logs/index.ts b/src/plugins/home/server/tutorials/f5_logs/index.ts new file mode 100644 index 0000000000000..4b4ae367ab46e --- /dev/null +++ b/src/plugins/home/server/tutorials/f5_logs/index.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function f5LogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'f5'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'f5Logs', + name: i18n.translate('home.tutorials.f5Logs.nameTitle', { + defaultMessage: 'F5 logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.f5Logs.shortDescription', { + defaultMessage: 'Collect F5 Big-IP Access Policy Manager logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.f5Logs.longDescription', { + defaultMessage: + 'This is a module for receiving Big-IP Access Policy Manager logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-f5.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/f5.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.f5Logs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-f5.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/f5_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/fortinet_logs/index.ts b/src/plugins/home/server/tutorials/fortinet_logs/index.ts new file mode 100644 index 0000000000000..d60d383016295 --- /dev/null +++ b/src/plugins/home/server/tutorials/fortinet_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function fortinetLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'fortinet'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'fortinetLogs', + name: i18n.translate('home.tutorials.fortinetLogs.nameTitle', { + defaultMessage: 'Fortinet logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.fortinetLogs.shortDescription', { + defaultMessage: 'Collect Fortinet FortiOS logs over syslog.', + }), + longDescription: i18n.translate('home.tutorials.fortinetLogs.longDescription', { + defaultMessage: + 'This is a module for Fortinet FortiOS logs sent in the syslog format. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-fortinet.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/fortinet.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.fortinetLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-fortinet.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/googlecloud_logs/index.ts b/src/plugins/home/server/tutorials/googlecloud_logs/index.ts new file mode 100644 index 0000000000000..482ad38ddfbcd --- /dev/null +++ b/src/plugins/home/server/tutorials/googlecloud_logs/index.ts @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function googlecloudLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'googlecloud'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'googlecloudLogs', + name: i18n.translate('home.tutorials.googlecloudLogs.nameTitle', { + defaultMessage: 'Google Cloud logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.googlecloudLogs.shortDescription', { + defaultMessage: 'Collect Google Cloud audit, firewall, and VPC flow logs.', + }), + longDescription: i18n.translate('home.tutorials.googlecloudLogs.longDescription', { + defaultMessage: + 'This is a module for Google Cloud logs. It supports reading audit, VPC flow, \ + and firewall logs that have been exported from Stackdriver to a Google Pub/Sub \ + topic sink. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-googlecloud.html', + }, + }), + euiIconType: 'logoGoogleG', + artifacts: { + dashboards: [ + { + id: '6576c480-73a2-11ea-a345-f985c61fe654', + linkLabel: i18n.translate( + 'home.tutorials.googlecloudLogs.artifacts.dashboards.linkLabel', + { + defaultMessage: 'Audit Logs Dashbaord', + } + ), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-googlecloud.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/googlecloud_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/gsuite_logs/index.ts b/src/plugins/home/server/tutorials/gsuite_logs/index.ts new file mode 100644 index 0000000000000..0f7c0d7077d39 --- /dev/null +++ b/src/plugins/home/server/tutorials/gsuite_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function gsuiteLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'gsuite'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'gsuiteLogs', + name: i18n.translate('home.tutorials.gsuiteLogs.nameTitle', { + defaultMessage: 'GSuite logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.gsuiteLogs.shortDescription', { + defaultMessage: 'Collect GSuite activity reports.', + }), + longDescription: i18n.translate('home.tutorials.gsuiteLogs.longDescription', { + defaultMessage: + 'This is a module for ingesting data from the different GSuite audit reports APIs. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-gsuite.html', + }, + }), + euiIconType: 'logoGoogleG', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.gsuiteLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-gsuite.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/haproxy_logs/index.ts b/src/plugins/home/server/tutorials/haproxy_logs/index.ts new file mode 100644 index 0000000000000..82ef405ffe839 --- /dev/null +++ b/src/plugins/home/server/tutorials/haproxy_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function haproxyLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'haproxy'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'haproxyLogs', + name: i18n.translate('home.tutorials.haproxyLogs.nameTitle', { + defaultMessage: 'HAProxy logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.haproxyLogs.shortDescription', { + defaultMessage: 'Collect HAProxy logs.', + }), + longDescription: i18n.translate('home.tutorials.haproxyLogs.longDescription', { + defaultMessage: + 'The module collects and parses logs from a ( `haproxy`) process. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-haproxy.html', + }, + }), + euiIconType: 'logoHAproxy', + artifacts: { + dashboards: [ + { + id: '3560d580-aa34-11e8-9c06-877f0445e3e0-ecs', + linkLabel: i18n.translate('home.tutorials.haproxyLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'HAProxy Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-haproxy.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/haproxy_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/icinga_logs/index.ts b/src/plugins/home/server/tutorials/icinga_logs/index.ts new file mode 100644 index 0000000000000..de494e5a15046 --- /dev/null +++ b/src/plugins/home/server/tutorials/icinga_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function icingaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'icinga'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'icingaLogs', + name: i18n.translate('home.tutorials.icingaLogs.nameTitle', { + defaultMessage: 'Icinga logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.icingaLogs.shortDescription', { + defaultMessage: 'Collect Icinga main, debug, and startup logs.', + }), + longDescription: i18n.translate('home.tutorials.icingaLogs.longDescription', { + defaultMessage: + 'The module parses the main, debug, and startup logs of [Icinga](https://www.icinga.com/products/icinga-2/). \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-icinga.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/icinga.svg', + artifacts: { + dashboards: [ + { + id: 'f693d260-2417-11e7-a83b-d5f4cebac9ff-ecs', + linkLabel: i18n.translate('home.tutorials.icingaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Icinga Main Log', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-icinga.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/icinga_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/imperva_logs/index.ts b/src/plugins/home/server/tutorials/imperva_logs/index.ts new file mode 100644 index 0000000000000..d723a4c2f49ef --- /dev/null +++ b/src/plugins/home/server/tutorials/imperva_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function impervaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'imperva'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'impervaLogs', + name: i18n.translate('home.tutorials.impervaLogs.nameTitle', { + defaultMessage: 'Imperva logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.impervaLogs.shortDescription', { + defaultMessage: 'Collect Imperva SecureSphere logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.impervaLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Imperva SecureSphere logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-imperva.html', + }, + }), + euiIconType: 'logoLogging', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.impervaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-imperva.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/infoblox_logs/index.ts b/src/plugins/home/server/tutorials/infoblox_logs/index.ts new file mode 100644 index 0000000000000..811f3110e7871 --- /dev/null +++ b/src/plugins/home/server/tutorials/infoblox_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function infobloxLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'infoblox'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'infobloxLogs', + name: i18n.translate('home.tutorials.infobloxLogs.nameTitle', { + defaultMessage: 'Infoblox logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.infobloxLogs.shortDescription', { + defaultMessage: 'Collect Infoblox NIOS logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.infobloxLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Infoblox NIOS logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-infoblox.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/infoblox.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.infobloxLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-infoblox.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts index b3be133767447..dc44ae6107b1e 100644 --- a/src/plugins/home/server/tutorials/iptables_logs/index.ts +++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts @@ -31,37 +31,39 @@ import { export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSchema { const moduleName = 'iptables'; - const platforms = ['DEB', 'RPM'] as const; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; return { id: 'iptablesLogs', name: i18n.translate('home.tutorials.iptablesLogs.nameTitle', { - defaultMessage: 'Iptables / Ubiquiti', + defaultMessage: 'Iptables logs', }), moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.iptablesLogs.shortDescription', { - defaultMessage: 'Collect and parse iptables and ip6tables logs or from Ubiqiti firewalls.', + defaultMessage: 'Collect iptables and ip6tables logs.', }), longDescription: i18n.translate('home.tutorials.iptablesLogs.longDescription', { defaultMessage: - 'This is a module for iptables and ip6tables logs. It parses logs \ -received over the network via syslog or from a file. Also, it understands the \ -prefix added by some Ubiquiti firewalls, which includes the rule set name, rule \ -number and the action performed on the traffic (allow/deny).. \ -[Learn more]({learnMoreLink}).', + 'This is a module for iptables and ip6tables logs. It parses logs received \ + over the network via syslog or from a file. Also, it understands the prefix \ + added by some Ubiquiti firewalls, which includes the rule set name, rule \ + number and the action performed on the traffic (allow/deny). \ + [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-iptables.html', }, }), - euiIconType: '/plugins/home/assets/logos/ubiquiti.svg', + euiIconType: '/plugins/home/assets/logos/linux.svg', artifacts: { - dashboards: [], - application: { - path: '/app/security', - label: i18n.translate('home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Security App', - }), - }, + dashboards: [ + { + id: 'ceefb9e0-1f51-11e9-93ed-f7e068f4aebb-ecs', + linkLabel: i18n.translate('home.tutorials.iptablesLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Iptables Overview', + }), + isOverview: true, + }, + ], exportedFields: { documentationUrl: '{config.docs.beats.filebeat}/exported-fields-iptables.html', }, diff --git a/src/plugins/home/server/tutorials/juniper_logs/index.ts b/src/plugins/home/server/tutorials/juniper_logs/index.ts new file mode 100644 index 0000000000000..45688baa5bcdf --- /dev/null +++ b/src/plugins/home/server/tutorials/juniper_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function juniperLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'juniper'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'juniperLogs', + name: i18n.translate('home.tutorials.juniperLogs.nameTitle', { + defaultMessage: 'Juniper Logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.juniperLogs.shortDescription', { + defaultMessage: 'Collect Juniper JUNOS logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.juniperLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Juniper JUNOS logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-juniper.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/juniper.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.juniperLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-juniper.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/kibana_logs/index.ts b/src/plugins/home/server/tutorials/kibana_logs/index.ts new file mode 100644 index 0000000000000..88286a230f771 --- /dev/null +++ b/src/plugins/home/server/tutorials/kibana_logs/index.ts @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function kibanaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'kibana'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'kibanaLogs', + name: i18n.translate('home.tutorials.kibanaLogs.nameTitle', { + defaultMessage: 'Kibana Logs', + }), + moduleName, + category: TutorialsCategory.LOGGING, + shortDescription: i18n.translate('home.tutorials.kibanaLogs.shortDescription', { + defaultMessage: 'Collect Kibana logs.', + }), + longDescription: i18n.translate('home.tutorials.kibanaLogs.longDescription', { + defaultMessage: 'This is the Kibana module. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-kibana.html', + }, + }), + euiIconType: 'logoKibana', + artifacts: { + dashboards: [], + application: { + label: i18n.translate('home.tutorials.kibanaLogs.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/discover#/', + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-kibana.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts index 32982cd1055a4..6998aaf9001e6 100644 --- a/src/plugins/home/server/tutorials/logstash_logs/index.ts +++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts @@ -38,13 +38,13 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche defaultMessage: 'Logstash logs', }), moduleName, - category: TutorialsCategory.LOGGING, + category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.logstashLogs.shortDescription', { - defaultMessage: 'Collect and parse debug and slow logs created by Logstash itself.', + defaultMessage: 'Collect Logstash main and slow logs.', }), longDescription: i18n.translate('home.tutorials.logstashLogs.longDescription', { defaultMessage: - 'The `logstash` Filebeat module parses debug and slow logs created by Logstash itself. \ + 'The modules parse Logstash regular logs and the slow log, it will support the plain text format and the JSON format. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-logstash.html', @@ -56,7 +56,7 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche { id: 'Filebeat-Logstash-Log-Dashboard-ecs', linkLabel: i18n.translate('home.tutorials.logstashLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Logstash logs dashboard', + defaultMessage: 'Logstash Logs', }), isOverview: true, }, @@ -66,7 +66,6 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/home/assets/logstash_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/microsoft_logs/index.ts b/src/plugins/home/server/tutorials/microsoft_logs/index.ts new file mode 100644 index 0000000000000..28739a5817ae7 --- /dev/null +++ b/src/plugins/home/server/tutorials/microsoft_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function microsoftLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'microsoft'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'microsoftLogs', + name: i18n.translate('home.tutorials.microsoftLogs.nameTitle', { + defaultMessage: 'Microsoft Defender ATP logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.microsoftLogs.shortDescription', { + defaultMessage: 'Collect Microsoft Defender ATP alerts.', + }), + longDescription: i18n.translate('home.tutorials.microsoftLogs.longDescription', { + defaultMessage: + 'Collect Microsoft Defender ATP alerts for use with Elastic Security. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-microsoft.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/microsoft.svg', + artifacts: { + dashboards: [ + { + id: '65402c30-ca6a-11ea-9d4d-9737a63aaa55', + linkLabel: i18n.translate('home.tutorials.microsoftLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Microsoft ATP Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-microsoft.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/microsoft_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/misp_logs/index.ts b/src/plugins/home/server/tutorials/misp_logs/index.ts new file mode 100644 index 0000000000000..050c22fb39523 --- /dev/null +++ b/src/plugins/home/server/tutorials/misp_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function mispLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'misp'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'mispLogs', + name: i18n.translate('home.tutorials.mispLogs.nameTitle', { + defaultMessage: 'MISP threat intel logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.mispLogs.shortDescription', { + defaultMessage: 'Collect MISP threat intelligence data with Filebeat.', + }), + longDescription: i18n.translate('home.tutorials.mispLogs.longDescription', { + defaultMessage: + 'This is a filebeat module for reading threat intel information from the MISP platform ( https://www.circl.lu/doc/misp/). It uses the httpjson input to access the MISP REST API interface. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-misp.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/misp.svg', + artifacts: { + dashboards: [ + { + id: 'c6cac9e0-f105-11e9-9a88-690b10c8ee99', + linkLabel: i18n.translate('home.tutorials.mispLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'MISP Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-misp.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/misp_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/mongodb_logs/index.ts b/src/plugins/home/server/tutorials/mongodb_logs/index.ts new file mode 100644 index 0000000000000..1c8db19a3873b --- /dev/null +++ b/src/plugins/home/server/tutorials/mongodb_logs/index.ts @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function mongodbLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'mongodb'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'mongodbLogs', + name: i18n.translate('home.tutorials.mongodbLogs.nameTitle', { + defaultMessage: 'MongoDB logs', + }), + moduleName, + category: TutorialsCategory.LOGGING, + shortDescription: i18n.translate('home.tutorials.mongodbLogs.shortDescription', { + defaultMessage: 'Collect MongoDB logs.', + }), + longDescription: i18n.translate('home.tutorials.mongodbLogs.longDescription', { + defaultMessage: + 'The module collects and parses logs created by [MongoDB](https://www.mongodb.com/). \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-mongodb.html', + }, + }), + euiIconType: 'logoMongodb', + artifacts: { + dashboards: [ + { + id: 'abcf35b0-0a82-11e8-bffe-ff7d4f68cf94-ecs', + linkLabel: i18n.translate('home.tutorials.mongodbLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'MongoDB Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-mongodb.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/mongodb_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/mssql_logs/index.ts b/src/plugins/home/server/tutorials/mssql_logs/index.ts new file mode 100644 index 0000000000000..8a18da20f2e6a --- /dev/null +++ b/src/plugins/home/server/tutorials/mssql_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function mssqlLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'mssql'; + const platforms = ['DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'mssqlLogs', + name: i18n.translate('home.tutorials.mssqlLogs.nameTitle', { + defaultMessage: 'MSSQL logs', + }), + moduleName, + category: TutorialsCategory.LOGGING, + shortDescription: i18n.translate('home.tutorials.mssqlLogs.shortDescription', { + defaultMessage: 'Collect MSSQL logs.', + }), + longDescription: i18n.translate('home.tutorials.mssqlLogs.longDescription', { + defaultMessage: + 'The module parses error logs created by MSSQL. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-mssql.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/microsoft.svg', + artifacts: { + dashboards: [], + application: { + label: i18n.translate('home.tutorials.mssqlLogs.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/discover#/', + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-mssql.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/netflow/common_instructions.ts b/src/plugins/home/server/tutorials/netflow/common_instructions.ts deleted file mode 100644 index 8fe24ba9c7994..0000000000000 --- a/src/plugins/home/server/tutorials/netflow/common_instructions.ts +++ /dev/null @@ -1,281 +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 { i18n } from '@kbn/i18n'; - -export function createCommonNetflowInstructions() { - return { - CONFIG: { - ON_PREM: { - OSX: [ - { - title: i18n.translate('home.tutorials.netflow.common.config.onPrem.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.netflow.common.config.onPrem.osxTextPre', { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config/logstash.yml`', - }, - }), - commands: ['modules:', ' - name: netflow', ' var.input.udp.port: '], - textPost: i18n.translate('home.tutorials.netflow.common.config.onPrem.osxTextPost', { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data.', - values: { - udpPort: '``', - }, - }), - }, - ], - WINDOWS: [ - { - title: i18n.translate('home.tutorials.netflow.common.config.onPrem.windowsTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate('home.tutorials.netflow.common.config.onPrem.windowsTextPre', { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config\\logstash.yml`', - }, - }), - commands: ['modules:', ' - name: netflow', ' var.input.udp.port: '], - textPost: i18n.translate( - 'home.tutorials.netflow.common.config.onPrem.windowsTextPost', - { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data.', - values: { - udpPort: '``', - }, - } - ), - }, - ], - }, - ON_PREM_ELASTIC_CLOUD: { - OSX: [ - { - title: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.osxTitle', - { - defaultMessage: 'Edit the configuration', - } - ), - textPre: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.osxTextPre', - { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config/logstash.yml`', - }, - } - ), - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ' var.elasticsearch.hosts: [ "" ]', - ' var.elasticsearch.username: elastic', - ' var.elasticsearch.password: ', - ], - textPost: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.osxTextPost', - { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data, \ - {esUrl} is the URL of Elasticsearch running on Elastic Cloud, and \ - {password} is the password of the {elastic} user.', - values: { - elastic: '`elastic`', - esUrl: '``', - password: '``', - udpPort: '``', - }, - } - ), - }, - ], - WINDOWS: [ - { - title: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.windowsTitle', - { - defaultMessage: 'Edit the configuration', - } - ), - textPre: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.windowsTextPre', - { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config\\logstash.yml`', - }, - } - ), - commands: [ - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ' var.elasticsearch.hosts: [ "" ]', - ' var.elasticsearch.username: elastic', - ' var.elasticsearch.password: ', - ], - textPost: i18n.translate( - 'home.tutorials.netflow.common.config.onPremElasticCloud.windowsTextPost', - { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data, \ - {esUrl} is the URL of Elasticsearch running on Elastic Cloud, and \ - {password} is the password of the {elastic} user.', - values: { - elastic: '`elastic`', - esUrl: '``', - password: '``', - udpPort: '``', - }, - } - ), - }, - ], - }, - ELASTIC_CLOUD: { - OSX: [ - { - title: i18n.translate('home.tutorials.netflow.common.config.elasticCloud.osxTitle', { - defaultMessage: 'Edit the configuration', - }), - textPre: i18n.translate( - 'home.tutorials.netflow.common.config.elasticCloud.osxTextPre', - { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config/logstash.yml`', - }, - } - ), - commands: [ - 'cloud.id: "{config.cloud.id}"', - 'cloud.auth: "elastic:"', - ' ', - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: i18n.translate( - 'home.tutorials.netflow.common.config.elasticCloud.osxTextPost', - { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data and \ - {password} is the password of the {elastic} user.', - values: { - elastic: '`elastic`', - password: '``', - udpPort: '``', - }, - } - ), - }, - ], - WINDOWS: [ - { - title: i18n.translate( - 'home.tutorials.netflow.common.config.elasticCloud.windowsTitle', - { - defaultMessage: 'Edit the configuration', - } - ), - textPre: i18n.translate( - 'home.tutorials.netflow.common.config.elasticCloud.windowsTextPre', - { - defaultMessage: 'Modify {logstashConfigPath} to set the configuration parameters:', - values: { - logstashConfigPath: '`config\\logstash.yml`', - }, - } - ), - commands: [ - 'cloud.id: "{config.cloud.id}"', - 'cloud.auth: "elastic:"', - ' ', - 'modules:', - ' - name: netflow', - ' var.input.udp.port: ', - ], - textPost: i18n.translate( - 'home.tutorials.netflow.common.config.elasticCloud.windowsTextPost', - { - defaultMessage: - 'Where {udpPort} is the UDP port on which Logstash will receive Netflow data and \ - {password} is the password of the {elastic} user.', - values: { - elastic: '`elastic`', - password: '``', - udpPort: '``', - }, - } - ), - }, - ], - }, - }, - SETUP: { - OSX: [ - { - title: i18n.translate('home.tutorials.netflow.common.setup.osxTitle', { - defaultMessage: 'Run the Netflow module', - }), - textPre: i18n.translate('home.tutorials.netflow.common.setup.osxTextPre', { - defaultMessage: 'Run:', - }), - commands: ['./bin/logstash --modules netflow --setup'], - textPost: i18n.translate('home.tutorials.netflow.common.setup.osxTextPost', { - defaultMessage: - 'The {setupOption} option creates a {netflowPrefix} index pattern in Elasticsearch and imports \ - Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.', - values: { - setupOption: '`--setup`', - netflowPrefix: '`netflow-*`', - }, - }), - }, - ], - WINDOWS: [ - { - title: i18n.translate('home.tutorials.netflow.common.setup.windowsTitle', { - defaultMessage: 'Run the Netflow module', - }), - textPre: i18n.translate('home.tutorials.netflow.common.setup.windowsTextPre', { - defaultMessage: 'Run:', - }), - commands: ['bin\\logstash --modules netflow --setup'], - textPost: i18n.translate('home.tutorials.netflow.common.setup.windowsTextPost', { - defaultMessage: - 'The {setupOption} option creates a {netflowPrefix} index pattern in Elasticsearch and imports \ - Kibana dashboards and visualizations. Omit this option for subsequent runs to avoid overwriting existing dashboards.', - values: { - setupOption: '`--setup`', - netflowPrefix: '`netflow-*`', - }, - }), - }, - ], - }, - }; -} diff --git a/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts b/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts deleted file mode 100644 index fbedc6abfbb8a..0000000000000 --- a/src/plugins/home/server/tutorials/netflow/elastic_cloud.ts +++ /dev/null @@ -1,58 +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 { i18n } from '@kbn/i18n'; - -import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; -import { createLogstashInstructions } from '../instructions/logstash_instructions'; -import { createCommonNetflowInstructions } from './common_instructions'; - -// TODO: compare with onPremElasticCloud and onPrem scenarios and extract out common bits -export function createElasticCloudInstructions() { - const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); - const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); - - return { - instructionSets: [ - { - title: i18n.translate('home.tutorials.netflow.elasticCloudInstructions.title', { - defaultMessage: 'Getting Started', - }), - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, - ], - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ELASTIC_CLOUD.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, - ], - }, - ], - }, - ], - }; -} diff --git a/src/plugins/home/server/tutorials/netflow/index.ts b/src/plugins/home/server/tutorials/netflow/index.ts deleted file mode 100644 index 5be30bbb152b7..0000000000000 --- a/src/plugins/home/server/tutorials/netflow/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; - -import { TutorialsCategory } from '../../services/tutorials'; -import { createOnPremInstructions } from './on_prem'; -import { createElasticCloudInstructions } from './elastic_cloud'; -import { createOnPremElasticCloudInstructions } from './on_prem_elastic_cloud'; - -export function netflowSpecProvider() { - const moduleName = 'netflow'; - return { - id: 'netflow', - name: 'Netflow', - moduleName, - category: TutorialsCategory.SECURITY_SOLUTION, - shortDescription: i18n.translate('home.tutorials.netflow.tutorialShortDescription', { - defaultMessage: 'Collect Netflow records sent by a Netflow exporter.', - }), - longDescription: i18n.translate('home.tutorials.netflow.tutorialLongDescription', { - defaultMessage: - 'The Logstash Netflow module collects and parses network flow data, \ -indexes the events into Elasticsearch, and installs a suite of Kibana dashboards. \ -This module support Netflow Version 5 and 9. [Learn more]({linkUrl}).', - values: { - linkUrl: '{config.docs.logstash}/netflow-module.html', - }, - }), - completionTimeMinutes: 10, - // previewImagePath: 'kibana-apache.png', TODO - onPrem: createOnPremInstructions(), - elasticCloud: createElasticCloudInstructions(), - onPremElasticCloud: createOnPremElasticCloudInstructions(), - }; -} diff --git a/src/plugins/home/server/tutorials/netflow/on_prem.ts b/src/plugins/home/server/tutorials/netflow/on_prem.ts deleted file mode 100644 index ef8c3e172af87..0000000000000 --- a/src/plugins/home/server/tutorials/netflow/on_prem.ts +++ /dev/null @@ -1,58 +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 { i18n } from '@kbn/i18n'; - -import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; -import { createLogstashInstructions } from '../instructions/logstash_instructions'; -import { createCommonNetflowInstructions } from './common_instructions'; - -// TODO: compare with onPremElasticCloud and elasticCloud scenarios and extract out common bits -export function createOnPremInstructions() { - const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); - const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); - - return { - instructionSets: [ - { - title: i18n.translate('home.tutorials.netflow.onPremInstructions.title', { - defaultMessage: 'Getting Started', - }), - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, - ], - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, - ], - }, - ], - }, - ], - }; -} diff --git a/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts b/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts deleted file mode 100644 index 85aa694970491..0000000000000 --- a/src/plugins/home/server/tutorials/netflow/on_prem_elastic_cloud.ts +++ /dev/null @@ -1,68 +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 { i18n } from '@kbn/i18n'; - -import { INSTRUCTION_VARIANT } from '../../../common/instruction_variant'; -import { createLogstashInstructions } from '../instructions/logstash_instructions'; -import { - createTrycloudOption1, - createTrycloudOption2, -} from '../instructions/onprem_cloud_instructions'; -import { createCommonNetflowInstructions } from './common_instructions'; - -// TODO: compare with onPrem and elasticCloud scenarios and extract out common bits -export function createOnPremElasticCloudInstructions() { - const COMMON_NETFLOW_INSTRUCTIONS = createCommonNetflowInstructions(); - const TRYCLOUD_OPTION1 = createTrycloudOption1(); - const TRYCLOUD_OPTION2 = createTrycloudOption2(); - const LOGSTASH_INSTRUCTIONS = createLogstashInstructions(); - - return { - instructionSets: [ - { - title: i18n.translate('home.tutorials.netflow.onPremElasticCloudInstructions.title', { - defaultMessage: 'Getting Started', - }), - instructionVariants: [ - { - id: INSTRUCTION_VARIANT.OSX, - instructions: [ - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2, - ...LOGSTASH_INSTRUCTIONS.INSTALL.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.OSX, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.OSX, - ], - }, - { - id: INSTRUCTION_VARIANT.WINDOWS, - instructions: [ - TRYCLOUD_OPTION1, - TRYCLOUD_OPTION2, - ...LOGSTASH_INSTRUCTIONS.INSTALL.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.CONFIG.ON_PREM_ELASTIC_CLOUD.WINDOWS, - ...COMMON_NETFLOW_INSTRUCTIONS.SETUP.WINDOWS, - ], - }, - ], - }, - ], - }; -} diff --git a/src/plugins/home/server/tutorials/netflow_logs/index.ts b/src/plugins/home/server/tutorials/netflow_logs/index.ts new file mode 100644 index 0000000000000..0e36bcafae580 --- /dev/null +++ b/src/plugins/home/server/tutorials/netflow_logs/index.ts @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function netflowLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'netflow'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'netflowLogs', + name: i18n.translate('home.tutorials.netflowLogs.nameTitle', { + defaultMessage: 'NetFlow / IPFIX Collector', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.netflowLogs.shortDescription', { + defaultMessage: 'Collect NetFlow and IPFIX flow records.', + }), + longDescription: i18n.translate('home.tutorials.netflowLogs.longDescription', { + defaultMessage: + 'This is a module for receiving NetFlow and IPFIX flow records over UDP. This input supports NetFlow versions 1, 5, 6, 7, 8 and 9, as well as IPFIX. For NetFlow versions older than 9, fields are mapped automatically to NetFlow v9. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-netflow.html', + }, + }), + euiIconType: 'logoBeats', + artifacts: { + dashboards: [ + { + id: '34e26884-161a-4448-9556-43b5bf2f62a2', + linkLabel: i18n.translate('home.tutorials.netflowLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Netflow Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-netflow.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/netscout_logs/index.ts b/src/plugins/home/server/tutorials/netscout_logs/index.ts new file mode 100644 index 0000000000000..b94e0df0bf795 --- /dev/null +++ b/src/plugins/home/server/tutorials/netscout_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function netscoutLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'netscout'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'netscoutLogs', + name: i18n.translate('home.tutorials.netscoutLogs.nameTitle', { + defaultMessage: 'Arbor Peakflow logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.netscoutLogs.shortDescription', { + defaultMessage: 'Collect Netscout Arbor Peakflow SP logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.netscoutLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Arbor Peakflow SP logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-netscout.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/netscout.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.netscoutLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-netscout.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/o365_logs/index.ts b/src/plugins/home/server/tutorials/o365_logs/index.ts new file mode 100644 index 0000000000000..2ee3f3550a0d7 --- /dev/null +++ b/src/plugins/home/server/tutorials/o365_logs/index.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function o365LogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'o365'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'o365Logs', + name: i18n.translate('home.tutorials.o365Logs.nameTitle', { + defaultMessage: 'Office 365 logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.o365Logs.shortDescription', { + defaultMessage: 'Collect Office 365 activity logs via the Office 365 API.', + }), + longDescription: i18n.translate('home.tutorials.o365Logs.longDescription', { + defaultMessage: + 'This is a module for Office 365 logs received via one of the Office 365 \ + API endpoints. It currently supports user, admin, system, and policy \ + actions and events from Office 365 and Azure AD activity logs exposed \ + by the Office 365 Management Activity API. \ + [Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-o365.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/o365.svg', + artifacts: { + dashboards: [ + { + id: '712e2c00-685d-11ea-8d6a-292ef5d68366', + linkLabel: i18n.translate('home.tutorials.o365Logs.artifacts.dashboards.linkLabel', { + defaultMessage: 'O365 Audit Dashboard', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-o365.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/o365_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/okta_logs/index.ts b/src/plugins/home/server/tutorials/okta_logs/index.ts new file mode 100644 index 0000000000000..6371d9848af2e --- /dev/null +++ b/src/plugins/home/server/tutorials/okta_logs/index.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 { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function oktaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'okta'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'oktaLogs', + name: i18n.translate('home.tutorials.oktaLogs.nameTitle', { + defaultMessage: 'Okta logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.oktaLogs.shortDescription', { + defaultMessage: 'Collect the Okta system log via the Okta API.', + }), + longDescription: i18n.translate('home.tutorials.oktaLogs.longDescription', { + defaultMessage: + 'The Okta module collects events from the [Okta API](https://developer.okta.com/docs/reference/). \ + Specifically this supports reading from the [Okta System Log API](https://developer.okta.com/docs/reference/api/system-log/). \ + [Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-okta.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/okta.svg', + artifacts: { + dashboards: [ + { + id: '749203a0-67b1-11ea-a76f-bf44814e437d', + linkLabel: i18n.translate('home.tutorials.oktaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Okta Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-okta.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/okta_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts index d2ddd19b930a2..4fd946c08e58a 100644 --- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts +++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts @@ -51,6 +51,7 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch learnMoreLink: '{config.docs.beats.metricbeat}/metricbeat-module-' + moduleName + '.html', }, }), + euiIconType: '/plugins/home/assets/logos/oracle.svg', artifacts: { application: { label: i18n.translate('home.tutorials.oracleMetrics.artifacts.application.label', { diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts index c4869a889a085..1d98b30b324ce 100644 --- a/src/plugins/home/server/tutorials/osquery_logs/index.ts +++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts @@ -40,23 +40,27 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.osqueryLogs.shortDescription', { - defaultMessage: 'Collect the result logs created by osqueryd.', + defaultMessage: 'Collect osquery logs in JSON format.', }), longDescription: i18n.translate('home.tutorials.osqueryLogs.longDescription', { defaultMessage: - 'The `osquery` Filebeat module collects the JSON result logs collected by `osqueryd`. \ -[Learn more]({learnMoreLink}).', + 'The module collects and decodes the result logs written by \ + [osqueryd](https://osquery.readthedocs.io/en/latest/introduction/using-osqueryd/) in \ + the JSON format. To set up osqueryd follow the osquery installation instructions for \ + your operating system and configure the `filesystem` logging driver (the default). \ + Make sure UTC timestamps are enabled. \ + [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-osquery.html', }, }), - euiIconType: 'logoOsquery', + euiIconType: '/plugins/home/assets/logos/osquery.svg', artifacts: { dashboards: [ { id: '69f5ae20-eb02-11e7-8f04-51231daa5b05-ecs', linkLabel: i18n.translate('home.tutorials.osqueryLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Osquery logs dashboard', + defaultMessage: 'Osquery Compliance Pack', }), isOverview: true, }, @@ -66,7 +70,6 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/home/assets/osquery_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/panw_logs/index.ts b/src/plugins/home/server/tutorials/panw_logs/index.ts new file mode 100644 index 0000000000000..5a0da7538b4c0 --- /dev/null +++ b/src/plugins/home/server/tutorials/panw_logs/index.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function panwLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'panw'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'panwLogs', + name: i18n.translate('home.tutorials.panwLogs.nameTitle', { + defaultMessage: 'Palo Alto Networks PAN-OS logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.panwLogs.shortDescription', { + defaultMessage: + 'Collect Palo Alto Networks PAN-OS threat and traffic logs over syslog or from a log file.', + }), + longDescription: i18n.translate('home.tutorials.panwLogs.longDescription', { + defaultMessage: + 'This is a module for Palo Alto Networks PAN-OS firewall monitoring \ + logs received over Syslog or read from a file. It currently supports \ + messages of Traffic and Threat types. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-panw.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/paloalto.svg', + artifacts: { + dashboards: [ + { + id: 'e40ba240-7572-11e9-976e-65a8f47cc4c1', + linkLabel: i18n.translate('home.tutorials.panwLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'PANW Network Flows', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-panw.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/panw_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts new file mode 100644 index 0000000000000..2676f8b52306c --- /dev/null +++ b/src/plugins/home/server/tutorials/rabbitmq_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function rabbitmqLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'rabbitmq'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'rabbitmqLogs', + name: i18n.translate('home.tutorials.rabbitmqLogs.nameTitle', { + defaultMessage: 'RabbitMQ logs', + }), + moduleName, + category: TutorialsCategory.LOGGING, + shortDescription: i18n.translate('home.tutorials.rabbitmqLogs.shortDescription', { + defaultMessage: 'Collect RabbitMQ logs.', + }), + longDescription: i18n.translate('home.tutorials.rabbitmqLogs.longDescription', { + defaultMessage: + 'This is the module for parsing [RabbitMQ log files](https://www.rabbitmq.com/logging.html) \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-rabbitmq.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/rabbitmq.svg', + artifacts: { + dashboards: [], + application: { + label: i18n.translate('home.tutorials.rabbitmqLogs.artifacts.application.label', { + defaultMessage: 'Discover', + }), + path: '/app/discover#/', + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-rabbitmq.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/radware_logs/index.ts b/src/plugins/home/server/tutorials/radware_logs/index.ts new file mode 100644 index 0000000000000..10f32c7b767ab --- /dev/null +++ b/src/plugins/home/server/tutorials/radware_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function radwareLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'radware'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'radwareLogs', + name: i18n.translate('home.tutorials.radwareLogs.nameTitle', { + defaultMessage: 'Radware DefensePro logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.radwareLogs.shortDescription', { + defaultMessage: 'Collect Radware DefensePro logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.radwareLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Radware DefensePro logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-radware.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/radware.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.radwareLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-radware.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/register.ts b/src/plugins/home/server/tutorials/register.ts index c48423edb2a07..67e9f5a406803 100644 --- a/src/plugins/home/server/tutorials/register.ts +++ b/src/plugins/home/server/tutorials/register.ts @@ -16,82 +16,115 @@ * specific language governing permissions and limitations * under the License. */ -import { systemLogsSpecProvider } from './system_logs'; -import { systemMetricsSpecProvider } from './system_metrics'; +import { activemqLogsSpecProvider } from './activemq_logs'; +import { activemqMetricsSpecProvider } from './activemq_metrics'; +import { aerospikeMetricsSpecProvider } from './aerospike_metrics'; import { apacheLogsSpecProvider } from './apache_logs'; import { apacheMetricsSpecProvider } from './apache_metrics'; +import { auditbeatSpecProvider } from './auditbeat'; +import { auditdLogsSpecProvider } from './auditd_logs'; +import { awsLogsSpecProvider } from './aws_logs'; +import { awsMetricsSpecProvider } from './aws_metrics'; +import { azureLogsSpecProvider } from './azure_logs'; +import { azureMetricsSpecProvider } from './azure_metrics'; +import { barracudaLogsSpecProvider } from './barracuda_logs'; +import { bluecoatLogsSpecProvider } from './bluecoat_logs'; +import { cefLogsSpecProvider } from './cef_logs'; +import { cephMetricsSpecProvider } from './ceph_metrics'; +import { checkpointLogsSpecProvider } from './checkpoint_logs'; +import { ciscoLogsSpecProvider } from './cisco_logs'; +import { cloudwatchLogsSpecProvider } from './cloudwatch_logs'; +import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; +import { consulMetricsSpecProvider } from './consul_metrics'; +import { corednsLogsSpecProvider } from './coredns_logs'; +import { corednsMetricsSpecProvider } from './coredns_metrics'; +import { couchbaseMetricsSpecProvider } from './couchbase_metrics'; +import { couchdbMetricsSpecProvider } from './couchdb_metrics'; +import { crowdstrikeLogsSpecProvider } from './crowdstrike_logs'; +import { cylanceLogsSpecProvider } from './cylance_logs'; +import { dockerMetricsSpecProvider } from './docker_metrics'; +import { dropwizardMetricsSpecProvider } from './dropwizard_metrics'; import { elasticsearchLogsSpecProvider } from './elasticsearch_logs'; +import { elasticsearchMetricsSpecProvider } from './elasticsearch_metrics'; +import { envoyproxyLogsSpecProvider } from './envoyproxy_logs'; +import { envoyproxyMetricsSpecProvider } from './envoyproxy_metrics'; +import { etcdMetricsSpecProvider } from './etcd_metrics'; +import { f5LogsSpecProvider } from './f5_logs'; +import { fortinetLogsSpecProvider } from './fortinet_logs'; +import { golangMetricsSpecProvider } from './golang_metrics'; +import { googlecloudLogsSpecProvider } from './googlecloud_logs'; +import { googlecloudMetricsSpecProvider } from './googlecloud_metrics'; +import { gsuiteLogsSpecProvider } from './gsuite_logs'; +import { haproxyLogsSpecProvider } from './haproxy_logs'; +import { haproxyMetricsSpecProvider } from './haproxy_metrics'; +import { ibmmqLogsSpecProvider } from './ibmmq_logs'; +import { ibmmqMetricsSpecProvider } from './ibmmq_metrics'; +import { icingaLogsSpecProvider } from './icinga_logs'; import { iisLogsSpecProvider } from './iis_logs'; +import { iisMetricsSpecProvider } from './iis_metrics'; +import { impervaLogsSpecProvider } from './imperva_logs'; +import { infobloxLogsSpecProvider } from './infoblox_logs'; +import { iptablesLogsSpecProvider } from './iptables_logs'; +import { juniperLogsSpecProvider } from './juniper_logs'; import { kafkaLogsSpecProvider } from './kafka_logs'; +import { kafkaMetricsSpecProvider } from './kafka_metrics'; +import { kibanaLogsSpecProvider } from './kibana_logs'; +import { kibanaMetricsSpecProvider } from './kibana_metrics'; +import { kubernetesMetricsSpecProvider } from './kubernetes_metrics'; import { logstashLogsSpecProvider } from './logstash_logs'; -import { nginxLogsSpecProvider } from './nginx_logs'; -import { nginxMetricsSpecProvider } from './nginx_metrics'; +import { logstashMetricsSpecProvider } from './logstash_metrics'; +import { memcachedMetricsSpecProvider } from './memcached_metrics'; +import { microsoftLogsSpecProvider } from './microsoft_logs'; +import { mispLogsSpecProvider } from './misp_logs'; +import { mongodbLogsSpecProvider } from './mongodb_logs'; +import { mongodbMetricsSpecProvider } from './mongodb_metrics'; +import { mssqlLogsSpecProvider } from './mssql_logs'; +import { mssqlMetricsSpecProvider } from './mssql_metrics'; +import { muninMetricsSpecProvider } from './munin_metrics'; import { mysqlLogsSpecProvider } from './mysql_logs'; import { mysqlMetricsSpecProvider } from './mysql_metrics'; -import { mongodbMetricsSpecProvider } from './mongodb_metrics'; +import { natsLogsSpecProvider } from './nats_logs'; +import { natsMetricsSpecProvider } from './nats_metrics'; +import { netflowLogsSpecProvider } from './netflow_logs'; +import { netscoutLogsSpecProvider } from './netscout_logs'; +import { nginxLogsSpecProvider } from './nginx_logs'; +import { nginxMetricsSpecProvider } from './nginx_metrics'; +import { o365LogsSpecProvider } from './o365_logs'; +import { oktaLogsSpecProvider } from './okta_logs'; +import { openmetricsMetricsSpecProvider } from './openmetrics_metrics'; +import { oracleMetricsSpecProvider } from './oracle_metrics'; import { osqueryLogsSpecProvider } from './osquery_logs'; +import { panwLogsSpecProvider } from './panw_logs'; import { phpfpmMetricsSpecProvider } from './php_fpm_metrics'; -import { postgresqlMetricsSpecProvider } from './postgresql_metrics'; import { postgresqlLogsSpecProvider } from './postgresql_logs'; +import { postgresqlMetricsSpecProvider } from './postgresql_metrics'; +import { prometheusMetricsSpecProvider } from './prometheus_metrics'; +import { rabbitmqLogsSpecProvider } from './rabbitmq_logs'; import { rabbitmqMetricsSpecProvider } from './rabbitmq_metrics'; +import { radwareLogsSpecProvider } from './radware_logs'; import { redisLogsSpecProvider } from './redis_logs'; import { redisMetricsSpecProvider } from './redis_metrics'; +import { redisenterpriseMetricsSpecProvider } from './redisenterprise_metrics'; +import { santaLogsSpecProvider } from './santa_logs'; +import { sonicwallLogsSpecProvider } from './sonicwall_logs'; +import { sophosLogsSpecProvider } from './sophos_logs'; +import { squidLogsSpecProvider } from './squid_logs'; +import { stanMetricsSpecProvider } from './stan_metrics'; +import { statsdMetricsSpecProvider } from './statsd_metrics'; import { suricataLogsSpecProvider } from './suricata_logs'; -import { dockerMetricsSpecProvider } from './docker_metrics'; -import { kubernetesMetricsSpecProvider } from './kubernetes_metrics'; -import { uwsgiMetricsSpecProvider } from './uwsgi_metrics'; -import { netflowSpecProvider } from './netflow'; +import { systemLogsSpecProvider } from './system_logs'; +import { systemMetricsSpecProvider } from './system_metrics'; +import { tomcatLogsSpecProvider } from './tomcat_logs'; import { traefikLogsSpecProvider } from './traefik_logs'; -import { cephMetricsSpecProvider } from './ceph_metrics'; -import { aerospikeMetricsSpecProvider } from './aerospike_metrics'; -import { couchbaseMetricsSpecProvider } from './couchbase_metrics'; -import { dropwizardMetricsSpecProvider } from './dropwizard_metrics'; -import { elasticsearchMetricsSpecProvider } from './elasticsearch_metrics'; -import { etcdMetricsSpecProvider } from './etcd_metrics'; -import { haproxyMetricsSpecProvider } from './haproxy_metrics'; -import { kafkaMetricsSpecProvider } from './kafka_metrics'; -import { kibanaMetricsSpecProvider } from './kibana_metrics'; -import { memcachedMetricsSpecProvider } from './memcached_metrics'; -import { muninMetricsSpecProvider } from './munin_metrics'; +import { traefikMetricsSpecProvider } from './traefik_metrics'; +import { uptimeMonitorsSpecProvider } from './uptime_monitors'; +import { uwsgiMetricsSpecProvider } from './uwsgi_metrics'; import { vSphereMetricsSpecProvider } from './vsphere_metrics'; -import { windowsMetricsSpecProvider } from './windows_metrics'; import { windowsEventLogsSpecProvider } from './windows_event_logs'; -import { golangMetricsSpecProvider } from './golang_metrics'; -import { logstashMetricsSpecProvider } from './logstash_metrics'; -import { prometheusMetricsSpecProvider } from './prometheus_metrics'; -import { zookeeperMetricsSpecProvider } from './zookeeper_metrics'; -import { uptimeMonitorsSpecProvider } from './uptime_monitors'; -import { cloudwatchLogsSpecProvider } from './cloudwatch_logs'; -import { awsMetricsSpecProvider } from './aws_metrics'; -import { mssqlMetricsSpecProvider } from './mssql_metrics'; -import { natsMetricsSpecProvider } from './nats_metrics'; -import { natsLogsSpecProvider } from './nats_logs'; +import { windowsMetricsSpecProvider } from './windows_metrics'; import { zeekLogsSpecProvider } from './zeek_logs'; -import { corednsMetricsSpecProvider } from './coredns_metrics'; -import { corednsLogsSpecProvider } from './coredns_logs'; -import { auditbeatSpecProvider } from './auditbeat'; -import { iptablesLogsSpecProvider } from './iptables_logs'; -import { ciscoLogsSpecProvider } from './cisco_logs'; -import { envoyproxyLogsSpecProvider } from './envoyproxy_logs'; -import { couchdbMetricsSpecProvider } from './couchdb_metrics'; -import { consulMetricsSpecProvider } from './consul_metrics'; -import { cockroachdbMetricsSpecProvider } from './cockroachdb_metrics'; -import { traefikMetricsSpecProvider } from './traefik_metrics'; -import { awsLogsSpecProvider } from './aws_logs'; -import { activemqLogsSpecProvider } from './activemq_logs'; -import { activemqMetricsSpecProvider } from './activemq_metrics'; -import { azureMetricsSpecProvider } from './azure_metrics'; -import { ibmmqLogsSpecProvider } from './ibmmq_logs'; -import { stanMetricsSpecProvider } from './stan_metrics'; -import { envoyproxyMetricsSpecProvider } from './envoyproxy_metrics'; -import { ibmmqMetricsSpecProvider } from './ibmmq_metrics'; -import { statsdMetricsSpecProvider } from './statsd_metrics'; -import { redisenterpriseMetricsSpecProvider } from './redisenterprise_metrics'; -import { openmetricsMetricsSpecProvider } from './openmetrics_metrics'; -import { oracleMetricsSpecProvider } from './oracle_metrics'; -import { iisMetricsSpecProvider } from './iis_metrics'; -import { azureLogsSpecProvider } from './azure_logs'; -import { googlecloudMetricsSpecProvider } from './googlecloud_metrics'; +import { zookeeperMetricsSpecProvider } from './zookeeper_metrics'; +import { zscalerLogsSpecProvider } from './zscaler_logs'; export const builtInTutorials = [ systemLogsSpecProvider, @@ -118,7 +151,7 @@ export const builtInTutorials = [ dockerMetricsSpecProvider, kubernetesMetricsSpecProvider, uwsgiMetricsSpecProvider, - netflowSpecProvider, + netflowLogsSpecProvider, traefikLogsSpecProvider, cephMetricsSpecProvider, aerospikeMetricsSpecProvider, @@ -170,4 +203,37 @@ export const builtInTutorials = [ iisMetricsSpecProvider, azureLogsSpecProvider, googlecloudMetricsSpecProvider, + auditdLogsSpecProvider, + barracudaLogsSpecProvider, + bluecoatLogsSpecProvider, + cefLogsSpecProvider, + checkpointLogsSpecProvider, + crowdstrikeLogsSpecProvider, + cylanceLogsSpecProvider, + f5LogsSpecProvider, + fortinetLogsSpecProvider, + googlecloudLogsSpecProvider, + gsuiteLogsSpecProvider, + haproxyLogsSpecProvider, + icingaLogsSpecProvider, + impervaLogsSpecProvider, + infobloxLogsSpecProvider, + juniperLogsSpecProvider, + kibanaLogsSpecProvider, + microsoftLogsSpecProvider, + mispLogsSpecProvider, + mongodbLogsSpecProvider, + mssqlLogsSpecProvider, + netscoutLogsSpecProvider, + o365LogsSpecProvider, + oktaLogsSpecProvider, + panwLogsSpecProvider, + rabbitmqLogsSpecProvider, + radwareLogsSpecProvider, + santaLogsSpecProvider, + sonicwallLogsSpecProvider, + sophosLogsSpecProvider, + squidLogsSpecProvider, + tomcatLogsSpecProvider, + zscalerLogsSpecProvider, ]; diff --git a/src/plugins/home/server/tutorials/santa_logs/index.ts b/src/plugins/home/server/tutorials/santa_logs/index.ts new file mode 100644 index 0000000000000..3cdab67a08b57 --- /dev/null +++ b/src/plugins/home/server/tutorials/santa_logs/index.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 { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function santaLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'santa'; + const platforms = ['OSX'] as const; + return { + id: 'santaLogs', + name: i18n.translate('home.tutorials.santaLogs.nameTitle', { + defaultMessage: 'Google Santa logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.santaLogs.shortDescription', { + defaultMessage: 'Collect Google Santa logs about process executions on MacOS.', + }), + longDescription: i18n.translate('home.tutorials.santaLogs.longDescription', { + defaultMessage: + 'The module collects and parses logs from [Google Santa](https://github.com/google/santa), \ + a security tool for macOS that monitors process executions and can blacklist/whitelist binaries. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-santa.html', + }, + }), + euiIconType: 'logoLogging', + artifacts: { + dashboards: [ + { + id: '161855f0-ff6a-11e8-93c5-d5ecd1b3e307-ecs', + linkLabel: i18n.translate('home.tutorials.santaLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Santa Overview', + }), + isOverview: true, + }, + ], + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-santa.html', + }, + }, + completionTimeMinutes: 10, + previewImagePath: '/plugins/home/assets/santa_logs/screenshot.png', + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/sonicwall_logs/index.ts b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts new file mode 100644 index 0000000000000..49cfa694f3888 --- /dev/null +++ b/src/plugins/home/server/tutorials/sonicwall_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function sonicwallLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'sonicwall'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'sonicwallLogs', + name: i18n.translate('home.tutorials.sonicwallLogs.nameTitle', { + defaultMessage: 'Sonicwall FW logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.sonicwallLogs.shortDescription', { + defaultMessage: 'Collect Sonicwall-FW logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.sonicwallLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Sonicwall-FW logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-sonicwall.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/sonicwall.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.radwareLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-sonicwall.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/sophos_logs/index.ts b/src/plugins/home/server/tutorials/sophos_logs/index.ts new file mode 100644 index 0000000000000..0c732fe954d3b --- /dev/null +++ b/src/plugins/home/server/tutorials/sophos_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function sophosLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'sophos'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'sophosLogs', + name: i18n.translate('home.tutorials.sophosLogs.nameTitle', { + defaultMessage: 'Sophos logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.sophosLogs.shortDescription', { + defaultMessage: 'Collect Sophos XG SFOS logs over syslog.', + }), + longDescription: i18n.translate('home.tutorials.sophosLogs.longDescription', { + defaultMessage: + 'This is a module for Sophos Products, currently it supports XG SFOS logs sent in the syslog format. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-sophos.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/sophos.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.sophosLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-sophos.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/squid_logs/index.ts b/src/plugins/home/server/tutorials/squid_logs/index.ts new file mode 100644 index 0000000000000..c697f728b8e0e --- /dev/null +++ b/src/plugins/home/server/tutorials/squid_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function squidLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'squid'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'squidLogs', + name: i18n.translate('home.tutorials.squidLogs.nameTitle', { + defaultMessage: 'Squid logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.squidLogs.shortDescription', { + defaultMessage: 'Collect Squid logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.squidLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Squid logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-squid.html', + }, + }), + euiIconType: 'logoLogging', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.squidLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-squid.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts index eec81b9496647..85ea3062f8b6d 100644 --- a/src/plugins/home/server/tutorials/suricata_logs/index.ts +++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts @@ -40,13 +40,13 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.suricataLogs.shortDescription', { - defaultMessage: 'Collect the result logs created by Suricata IDS/IPS/NSM.', + defaultMessage: 'Collect Suricata IDS/IPS/NSM logs.', }), longDescription: i18n.translate('home.tutorials.suricataLogs.longDescription', { defaultMessage: - 'The `suricata` Filebeat module collects the logs from the \ -[Suricata Eve JSON output](https://suricata.readthedocs.io/en/latest/output/eve/eve-json-format.html). \ -[Learn more]({learnMoreLink}).', + 'This is a module to the Suricata IDS/IPS/NSM log. It parses logs that are \ + in the [Suricata Eve JSON format](https://suricata.readthedocs.io/en/latest/output/eve/eve-json-format.html). \ + [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-suricata.html', }, @@ -55,9 +55,9 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche artifacts: { dashboards: [ { - id: '69f5ae20-eb02-11e7-8f04-51231daa5b05', + id: '78289c40-86da-11e8-b59d-21efb914e65c-ecs', linkLabel: i18n.translate('home.tutorials.suricataLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Suricata logs dashboard', + defaultMessage: 'Suricata Events Overview', }), isOverview: true, }, diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts index f39df25461a5f..f94098399938f 100644 --- a/src/plugins/home/server/tutorials/system_logs/index.ts +++ b/src/plugins/home/server/tutorials/system_logs/index.ts @@ -31,33 +31,32 @@ import { export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema { const moduleName = 'system'; - const platforms = ['OSX', 'DEB', 'RPM'] as const; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; return { id: 'systemLogs', name: i18n.translate('home.tutorials.systemLogs.nameTitle', { defaultMessage: 'System logs', }), moduleName, - category: TutorialsCategory.LOGGING, + category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.systemLogs.shortDescription', { - defaultMessage: 'Collect and parse logs written by the local Syslog server.', + defaultMessage: 'Collect system logs of common Unix/Linux based distributions.', }), longDescription: i18n.translate('home.tutorials.systemLogs.longDescription', { defaultMessage: - 'The `system` Filebeat module collects and parses logs created by the system logging service of common \ -Unix/Linux based distributions. This module is not available on Windows. \ + 'The module collects and parses logs created by the system logging service of common Unix/Linux based distributions. \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-system.html', }, }), - euiIconType: '/plugins/home/assets/logos/system.svg', + euiIconType: 'logoLogging', artifacts: { dashboards: [ { id: 'Filebeat-syslog-dashboard-ecs', linkLabel: i18n.translate('home.tutorials.systemLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'System logs dashboard', + defaultMessage: 'System Syslog Dashboard', }), isOverview: true, }, @@ -67,7 +66,6 @@ Unix/Linux based distributions. This module is not available on Windows. \ }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/home/assets/system_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/tomcat_logs/index.ts b/src/plugins/home/server/tutorials/tomcat_logs/index.ts new file mode 100644 index 0000000000000..4b3c9f7357719 --- /dev/null +++ b/src/plugins/home/server/tutorials/tomcat_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function tomcatLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'tomcat'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'tomcatLogs', + name: i18n.translate('home.tutorials.tomcatLogs.nameTitle', { + defaultMessage: 'Tomcat logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.tomcatLogs.shortDescription', { + defaultMessage: 'Collect Apache Tomcat logs over syslog or from a file.', + }), + longDescription: i18n.translate('home.tutorials.tomcatLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Apache Tomcat logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-tomcat.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/tomcat.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.tomcatLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-tomcat.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts index 0a84dcb081883..8bcc806ad59a0 100644 --- a/src/plugins/home/server/tutorials/traefik_logs/index.ts +++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts @@ -38,13 +38,13 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem defaultMessage: 'Traefik logs', }), moduleName, - category: TutorialsCategory.LOGGING, + category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.traefikLogs.shortDescription', { - defaultMessage: 'Collect and parse access logs created by the Traefik Proxy.', + defaultMessage: 'Collect Traefik access logs.', }), longDescription: i18n.translate('home.tutorials.traefikLogs.longDescription', { defaultMessage: - 'The `traefik` Filebeat module parses access logs created by Traefik. \ + 'The module parses access logs created by [Træfik](https://traefik.io/). \ [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-traefik.html', @@ -56,7 +56,7 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem { id: 'Filebeat-Traefik-Dashboard-ecs', linkLabel: i18n.translate('home.tutorials.traefikLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Traefik logs dashboard', + defaultMessage: 'Traefik Access Logs', }), isOverview: true, }, @@ -66,7 +66,6 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem }, }, completionTimeMinutes: 10, - previewImagePath: '/plugins/home/assets/traefik_logs/screenshot.png', onPrem: onPremInstructions(moduleName, platforms, context), elasticCloud: cloudInstructions(moduleName, platforms), onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts index e39dcd3409490..dbc07d27b4f8a 100644 --- a/src/plugins/home/server/tutorials/zeek_logs/index.ts +++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts @@ -31,7 +31,7 @@ import { export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { const moduleName = 'zeek'; - const platforms = ['OSX', 'DEB', 'RPM'] as const; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; return { id: 'zeekLogs', name: i18n.translate('home.tutorials.zeekLogs.nameTitle', { @@ -40,13 +40,13 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { moduleName, category: TutorialsCategory.SECURITY_SOLUTION, shortDescription: i18n.translate('home.tutorials.zeekLogs.shortDescription', { - defaultMessage: 'Collect the logs created by Zeek/Bro.', + defaultMessage: 'Collect Zeek network security monitoring logs.', }), longDescription: i18n.translate('home.tutorials.zeekLogs.longDescription', { defaultMessage: - 'The `zeek` Filebeat module collects the logs from \ -[Zeek](https://www.zeek.org//documentation/index.html). \ -[Learn more]({learnMoreLink}).', + 'This is a module for Zeek, which used to be called Bro. It parses logs \ + that are in the [Zeek JSON format](https://www.zeek.org/manual/release/logs/index.html). \ + [Learn more]({learnMoreLink}).', values: { learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-zeek.html', }, @@ -57,7 +57,7 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema { { id: '7cbb5410-3700-11e9-aa6d-ff445a78330c', linkLabel: i18n.translate('home.tutorials.zeekLogs.artifacts.dashboards.linkLabel', { - defaultMessage: 'Zeek logs dashboard', + defaultMessage: 'Zeek Overview', }), isOverview: true, }, diff --git a/src/plugins/home/server/tutorials/zscaler_logs/index.ts b/src/plugins/home/server/tutorials/zscaler_logs/index.ts new file mode 100644 index 0000000000000..800702fe7da96 --- /dev/null +++ b/src/plugins/home/server/tutorials/zscaler_logs/index.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { TutorialsCategory } from '../../services/tutorials'; +import { + onPremInstructions, + cloudInstructions, + onPremCloudInstructions, +} from '../instructions/filebeat_instructions'; +import { + TutorialContext, + TutorialSchema, +} from '../../services/tutorials/lib/tutorials_registry_types'; + +export function zscalerLogsSpecProvider(context: TutorialContext): TutorialSchema { + const moduleName = 'zscaler'; + const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const; + return { + id: 'zscalerLogs', + name: i18n.translate('home.tutorials.zscalerLogs.nameTitle', { + defaultMessage: 'Zscaler Logs', + }), + moduleName, + category: TutorialsCategory.SECURITY_SOLUTION, + shortDescription: i18n.translate('home.tutorials.zscalerLogs.shortDescription', { + defaultMessage: 'This is a module for receiving Zscaler NSS logs over Syslog or a file.', + }), + longDescription: i18n.translate('home.tutorials.zscalerLogs.longDescription', { + defaultMessage: + 'This is a module for receiving Zscaler NSS logs over Syslog or a file. \ +[Learn more]({learnMoreLink}).', + values: { + learnMoreLink: '{config.docs.beats.filebeat}/filebeat-module-zscaler.html', + }, + }), + euiIconType: '/plugins/home/assets/logos/zscaler.svg', + artifacts: { + dashboards: [], + application: { + path: '/app/security', + label: i18n.translate('home.tutorials.zscalerLogs.artifacts.dashboards.linkLabel', { + defaultMessage: 'Security App', + }), + }, + exportedFields: { + documentationUrl: '{config.docs.beats.filebeat}/exported-fields-zscaler.html', + }, + }, + completionTimeMinutes: 10, + onPrem: onPremInstructions(moduleName, platforms, context), + elasticCloud: cloudInstructions(moduleName, platforms), + onPremElasticCloud: onPremCloudInstructions(moduleName, platforms), + }; +} diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx index 39c0add40e9ad..076cda484d808 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field_container.tsx @@ -26,31 +26,31 @@ import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; import { CreateEditField } from './create_edit_field'; -export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName: string }>; +export type CreateEditFieldContainerProps = RouteComponentProps<{ id: string; fieldName?: string }>; const CreateEditFieldCont: React.FC = ({ ...props }) => { const { setBreadcrumbs, data } = useKibana().services; const [indexPattern, setIndexPattern] = useState(); + const fieldName = + props.match.params.fieldName && decodeURIComponent(props.match.params.fieldName); useEffect(() => { data.indexPatterns.get(props.match.params.id).then((ip: IndexPattern) => { setIndexPattern(ip); if (ip) { setBreadcrumbs( - props.match.params.fieldName - ? getEditFieldBreadcrumbs(ip, props.match.params.fieldName) - : getCreateFieldBreadcrumbs(ip) + fieldName ? getEditFieldBreadcrumbs(ip, fieldName) : getCreateFieldBreadcrumbs(ip) ); } }); - }, [props.match.params.id, props.match.params.fieldName, setBreadcrumbs, data.indexPatterns]); + }, [props.match.params.id, fieldName, setBreadcrumbs, data.indexPatterns]); if (indexPattern) { return ( ); } else { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts new file mode 100644 index 0000000000000..0e3ee27476fcc --- /dev/null +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.test.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getPath } from './utils'; +import { IndexPatternField, IndexPattern } from '../../../../../data/public'; + +test('getPath() should encode "fieldName"', () => { + expect( + getPath( + ({ name: 'Memory: Allocated Bytes/sec' } as unknown) as IndexPatternField, + ({ id: 'id' } as unknown) as IndexPattern + ) + ).toMatchInlineSnapshot(`"/patterns/id/field/Memory%3A%20Allocated%20Bytes%2Fsec"`); +}); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts index 91c5cc1afdb49..a94ed60b7aed5 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/utils.ts @@ -117,7 +117,7 @@ export function getTabs( } export function getPath(field: IndexPatternField, indexPattern: IndexPattern) { - return `/patterns/${indexPattern?.id}/field/${field.name}`; + return `/patterns/${indexPattern?.id}/field/${encodeURIComponent(field.name)}`; } const allTypesDropDown = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index add45a07e0c5f..d3f97e7f8aeb7 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -102,6 +102,7 @@ export async function mountManagementSection( ); return () => { + chrome.docTitle.reset(); ReactDOM.unmountComponentAtNode(params.element); }; } diff --git a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx index 90e875fd43432..aa473095aaf3f 100644 --- a/src/plugins/input_control_vis/public/components/editor/control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/control_editor.tsx @@ -18,9 +18,8 @@ */ import React, { PureComponent, ChangeEvent } from 'react'; -import { InjectedIntlProps } from 'react-intl'; +import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, EuiButtonIcon, diff --git a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx index 19046f7f62fba..a9f04a86f8d03 100644 --- a/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx +++ b/src/plugins/input_control_vis/public/components/editor/controls_tab.tsx @@ -18,9 +18,8 @@ */ import React, { PureComponent } from 'react'; -import { InjectedIntlProps } from 'react-intl'; +import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, diff --git a/src/plugins/input_control_vis/public/components/editor/field_select.tsx b/src/plugins/input_control_vis/public/components/editor/field_select.tsx index 2885cbf24553f..24ebfc46ae25f 100644 --- a/src/plugins/input_control_vis/public/components/editor/field_select.tsx +++ b/src/plugins/input_control_vis/public/components/editor/field_select.tsx @@ -19,9 +19,8 @@ import _ from 'lodash'; import React, { Component } from 'react'; -import { InjectedIntlProps } from 'react-intl'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { injectI18n, FormattedMessage, InjectedIntlProps } from '@kbn/i18n/react'; import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { IIndexPattern, IFieldType } from '../../../../data/public'; diff --git a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx index 66fdbca64f053..6da5be450beb0 100644 --- a/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx +++ b/src/plugins/input_control_vis/public/components/editor/index_pattern_select_form_row.tsx @@ -18,16 +18,15 @@ */ import React, { ComponentType } from 'react'; -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntlProps } from '@kbn/i18n/react'; import { EuiFormRow } from '@elastic/eui'; -import { InjectedIntlProps } from 'react-intl'; -import { IndexPatternSelect } from 'src/plugins/data/public'; +import { IndexPatternSelectProps } from 'src/plugins/data/public'; export type IndexPatternSelectFormRowUiProps = InjectedIntlProps & { onChange: (opt: any) => void; indexPatternId: string; controlIndex: number; - IndexPatternSelect: ComponentType; + IndexPatternSelect: ComponentType; }; function IndexPatternSelectFormRowUi(props: IndexPatternSelectFormRowUiProps) { diff --git a/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx index cf105d16f0535..2ebf19f522e9e 100644 --- a/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/list_control_editor.tsx @@ -25,14 +25,14 @@ import { EuiFormRow, EuiFieldNumber, EuiSwitch, EuiSelect } from '@elastic/eui'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface ListControlEditorState { isLoadingFieldType: boolean; isStringField: boolean; prevFieldName: string; - IndexPatternSelect: ComponentType | null; + IndexPatternSelect: ComponentType | null; } interface ListControlEditorProps { diff --git a/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 92169705f0a10..7013e069f8e9d 100644 --- a/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelectProps } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { @@ -42,7 +42,7 @@ interface RangeControlEditorProps { } interface RangeControlEditorState { - IndexPatternSelect: ComponentType | null; + IndexPatternSelect: ComponentType | null; } function filterField(field: IFieldType) { diff --git a/src/plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx index 8ca93a302be89..e34989427be21 100644 --- a/src/plugins/input_control_vis/public/components/vis/list_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/list_control.tsx @@ -19,9 +19,8 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; -import { injectI18n } from '@kbn/i18n/react'; -import { InjectedIntlProps } from 'react-intl'; +import { injectI18n, InjectedIntlProps } from '@kbn/i18n/react'; import { EuiFieldText, EuiComboBox } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormRow } from './form_row'; diff --git a/src/plugins/kibana_overview/README.md b/src/plugins/kibana_overview/README.md new file mode 100644 index 0000000000000..ad0cbfdf7013b --- /dev/null +++ b/src/plugins/kibana_overview/README.md @@ -0,0 +1,9 @@ +# kibana-overview + +> An overview page highlighting Kibana apps + +--- + +## Development + +See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions setting up your development environment. diff --git a/src/plugins/data/public/ui/shard_failure_modal/index.ts b/src/plugins/kibana_overview/common/index.ts similarity index 82% rename from src/plugins/data/public/ui/shard_failure_modal/index.ts rename to src/plugins/kibana_overview/common/index.ts index e5af9633e73b7..3bdfbee1081ad 100644 --- a/src/plugins/data/public/ui/shard_failure_modal/index.ts +++ b/src/plugins/kibana_overview/common/index.ts @@ -17,5 +17,7 @@ * under the License. */ -export { ShardFailureRequest } from './shard_failure_types'; -export { ShardFailureOpenModalButton } from './shard_failure_open_modal_button'; +export const PLUGIN_ID = 'kibanaOverview'; +export const PLUGIN_NAME = 'Overview'; +export const PLUGIN_PATH = `/app/kibana_overview`; +export const PLUGIN_ICON = 'logoKibana'; diff --git a/src/plugins/kibana_overview/kibana.json b/src/plugins/kibana_overview/kibana.json new file mode 100644 index 0000000000000..9ddcaabdaed6b --- /dev/null +++ b/src/plugins/kibana_overview/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "kibanaOverview", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["navigation", "data", "home"], + "optionalPlugins": ["newsfeed"], + "requiredBundles": ["kibanaReact", "newsfeed"] +} diff --git a/src/plugins/kibana_overview/public/application.tsx b/src/plugins/kibana_overview/public/application.tsx new file mode 100644 index 0000000000000..1bf3fe07c36a8 --- /dev/null +++ b/src/plugins/kibana_overview/public/application.tsx @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +import { NewsfeedApiEndpoint } from '../../../../src/plugins/newsfeed/public'; +import { AppMountParameters, CoreStart } from '../../../../src/core/public'; +import { AppPluginStartDependencies } from './types'; +import { KibanaOverviewApp } from './components/app'; + +export const renderApp = ( + core: CoreStart, + deps: AppPluginStartDependencies, + { appBasePath, element }: AppMountParameters +) => { + const { notifications, http } = core; + const { newsfeed, home, navigation } = deps; + const newsfeed$ = newsfeed?.createNewsFeed$(NewsfeedApiEndpoint.KIBANA_ANALYTICS); + const navLinks = core.chrome.navLinks.getAll(); + const solutions = home.featureCatalogue + .getSolutions() + .filter(({ id }) => id !== 'kibana') + .filter(({ id }) => navLinks.find(({ category, hidden }) => !hidden && category?.id === id)); + const features = home.featureCatalogue.get(); + + ReactDOM.render( + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/src/plugins/kibana_overview/public/assets/kibana_canvas_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_canvas_dark.svg new file mode 100644 index 0000000000000..c86a3a89924c8 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_canvas_dark.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_canvas_light.svg b/src/plugins/kibana_overview/public/assets/kibana_canvas_light.svg new file mode 100644 index 0000000000000..d51560cb915c9 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_canvas_light.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_dashboard_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_dashboard_dark.svg new file mode 100644 index 0000000000000..834dd98d60e4c --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_dashboard_dark.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_dashboard_light.svg b/src/plugins/kibana_overview/public/assets/kibana_dashboard_light.svg new file mode 100644 index 0000000000000..958d25362c439 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_dashboard_light.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_discover_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_discover_dark.svg new file mode 100644 index 0000000000000..cf3116ae7f36a --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_discover_dark.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_discover_light.svg b/src/plugins/kibana_overview/public/assets/kibana_discover_light.svg new file mode 100644 index 0000000000000..6e039d03bef89 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_discover_light.svg @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_graph_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_graph_dark.svg new file mode 100644 index 0000000000000..ea43adf3390d0 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_graph_dark.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_graph_light.svg b/src/plugins/kibana_overview/public/assets/kibana_graph_light.svg new file mode 100644 index 0000000000000..c4505209a20bd --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_graph_light.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_maps_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_maps_dark.svg new file mode 100644 index 0000000000000..a6c53012a5e90 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_maps_dark.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_maps_light.svg b/src/plugins/kibana_overview/public/assets/kibana_maps_light.svg new file mode 100644 index 0000000000000..06a3ee5177eb9 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_maps_light.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_ml_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_ml_dark.svg new file mode 100644 index 0000000000000..574760e523f51 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_ml_dark.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_ml_light.svg b/src/plugins/kibana_overview/public/assets/kibana_ml_light.svg new file mode 100644 index 0000000000000..2375d78ce61f3 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_ml_light.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_montage_dark.svg b/src/plugins/kibana_overview/public/assets/kibana_montage_dark.svg new file mode 100644 index 0000000000000..7a476e4b19ae1 --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_montage_dark.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/kibana_montage_light.svg b/src/plugins/kibana_overview/public/assets/kibana_montage_light.svg new file mode 100644 index 0000000000000..83f48a0d31a0c --- /dev/null +++ b/src/plugins/kibana_overview/public/assets/kibana_montage_light.svg @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png new file mode 100644 index 0000000000000..86ac827f06a77 Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_dark_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png new file mode 100644 index 0000000000000..527a09aad05ec Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_enterprise_search_light_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png new file mode 100644 index 0000000000000..c9dd85ee07f35 Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_observability_dark_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png new file mode 100644 index 0000000000000..85120b906c967 Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_observability_light_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png b/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png new file mode 100644 index 0000000000000..24f902bff090b Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_security_solution_dark_2x.png differ diff --git a/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png b/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png new file mode 100644 index 0000000000000..2b35af848f912 Binary files /dev/null and b/src/plugins/kibana_overview/public/assets/solutions_security_solution_light_2x.png differ diff --git a/src/plugins/kibana_overview/public/components/_index.scss b/src/plugins/kibana_overview/public/components/_index.scss new file mode 100644 index 0000000000000..b8857d171728f --- /dev/null +++ b/src/plugins/kibana_overview/public/components/_index.scss @@ -0,0 +1 @@ +@import 'overview'; diff --git a/src/plugins/kibana_overview/public/components/_overview.scss b/src/plugins/kibana_overview/public/components/_overview.scss new file mode 100644 index 0000000000000..74a58122d4851 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/_overview.scss @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.kbnOverviewWrapper { + background-color: $euiColorEmptyShade; + display: flex; + flex-direction: column; + min-height: calc(100vh - #{$euiHeaderHeightCompensation}); +} + +.kbnOverviewContent { + margin: 0 auto; + max-width: 1200px; + padding: $euiSizeXL $euiSize; + width: 100%; + + // Ensure card heights are stretched equally when wrapped with this element + .kbnRedirectCrossAppLinks { + align-items: flex-start; + display: flex; + flex: 1; + flex-direction: column; + } +} + +.kbnOverviewApps__item { + .kbnOverviewApps__group--primary & { + @include euiBreakpoint('m', 'l', 'xl') { + max-width: calc(50% - #{$euiSizeM * 2}); + } + } + + .kbnOverviewApps__group--secondary & { + @include euiBreakpoint('m', 'l', 'xl') { + max-width: calc(25% - #{$euiSizeM * 2}); + } + } +} + +.kbnOverviewNews__content article { + & + article { + margin-top: $euiSizeL; + } + + &, + header { + & > * + * { + margin-top: $euiSizeXS; + } + } + + h3 { + font-weight: inherit; + } +} + +.kbnOverviewMore__item { + @include euiBreakpoint('m', 'l', 'xl') { + max-width: calc(33.333333333333333% - #{$euiSizeM * 2}); + } +} + +.kbnOverviewSolution__icon { + background-color: $euiColorEmptyShade !important; + box-shadow: none !important; + height: $euiSizeL * 2; + padding: $euiSizeM; + width: $euiSizeL * 2; +} + +.kbnOverviewSupplements--noNews .kbnOverviewMore { + h2 { + @include euiBreakpoint('m', 'l', 'xl') { + text-align: center; + } + } + + .kbnOverviewMore__content { + @include euiBreakpoint('m', 'l', 'xl') { + justify-content: center; + } + } +} + +.kbnOverviewData--expanded { + flex-direction: column; + + &, + & > * { + margin-bottom: 0 !important; + margin-top: 0 !important; + } +} + +// Accounting for no `flush="both"` prop on EuiButtonEmpty +.kbnOverviewDataAdd__actionButton { + margin-right: 0; +} + +.kbnOverviewDataManage__item:not(:only-child) { + @include euiBreakpoint('m', 'l', 'xl') { + flex: 0 0 calc(50% - #{$euiSizeM * 2}); + } +} diff --git a/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap new file mode 100644 index 0000000000000..42623abd79ac0 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AddData render 1`] = ` +
+ + + +

+ +

+
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + +
+`; diff --git a/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx b/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx new file mode 100644 index 0000000000000..f5cdbd9e27e28 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/add_data/add_data.test.tsx @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { AddData } from './add_data'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { FeatureCatalogueCategory } from 'src/plugins/home/public'; + +const mockFeatures = [ + { + category: FeatureCatalogueCategory.DATA, + description: 'Ingest data from popular apps and services.', + showOnHomePage: true, + icon: 'indexOpen', + id: 'home_tutorial_directory', + order: 500, + path: '/app/home#/tutorial_directory', + title: 'Ingest data', + }, + { + category: FeatureCatalogueCategory.ADMIN, + description: 'Add and manage your fleet of Elastic Agents and integrations.', + showOnHomePage: true, + icon: 'indexManagementApp', + id: 'ingestManager', + order: 510, + path: '/app/ingestManager', + title: 'Add Elastic Agent', + }, + { + category: FeatureCatalogueCategory.DATA, + description: 'Import your own CSV, NDJSON, or log file', + showOnHomePage: true, + icon: 'document', + id: 'ml_file_data_visualizer', + order: 520, + path: '/app/ml#/filedatavisualizer', + title: 'Upload a file', + }, +]; + +const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); + +describe('AddData', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_overview/public/components/add_data/add_data.tsx b/src/plugins/kibana_overview/public/components/add_data/add_data.tsx new file mode 100644 index 0000000000000..e29c2a08395cf --- /dev/null +++ b/src/plugins/kibana_overview/public/components/add_data/add_data.tsx @@ -0,0 +1,108 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; +// @ts-expect-error untyped component +import { Synopsis } from '../synopsis'; + +interface Props { + addBasePath: (path: string) => string; + features: FeatureCatalogueEntry[]; +} + +export const AddData: FC = ({ addBasePath, features }) => { + const { + services: { application }, + } = useKibana(); + + return ( +
+ + + +

+ +

+
+
+ + +
+ + + +
+
+
+ + + + + {features.map((feature) => ( + + + + + + ))} + +
+ ); +}; + +AddData.propTypes = { + addBasePath: PropTypes.func.isRequired, + features: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + showOnHomePage: PropTypes.bool.isRequired, + category: PropTypes.string.isRequired, + order: PropTypes.number, + }) + ), +}; diff --git a/src/plugins/data/public/ui/index_pattern_select/index.ts b/src/plugins/kibana_overview/public/components/add_data/index.ts similarity index 95% rename from src/plugins/data/public/ui/index_pattern_select/index.ts rename to src/plugins/kibana_overview/public/components/add_data/index.ts index 30dcb43557507..a7d465d177636 100644 --- a/src/plugins/data/public/ui/index_pattern_select/index.ts +++ b/src/plugins/kibana_overview/public/components/add_data/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './index_pattern_select'; +export * from './add_data'; diff --git a/src/plugins/kibana_overview/public/components/app.tsx b/src/plugins/kibana_overview/public/components/app.tsx new file mode 100644 index 0000000000000..bf9211f49a18e --- /dev/null +++ b/src/plugins/kibana_overview/public/components/app.tsx @@ -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 React, { useEffect, useState } from 'react'; +import { Observable } from 'rxjs'; +import { I18nProvider } from '@kbn/i18n/react'; +import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { CoreStart } from 'src/core/public'; +import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import { FetchResult } from 'src/plugins/newsfeed/public'; +import { FeatureCatalogueEntry, FeatureCatalogueSolution } from 'src/plugins/home/public'; +import { Overview } from './overview'; + +interface KibanaOverviewAppDeps { + basename: string; + notifications: CoreStart['notifications']; + http: CoreStart['http']; + navigation: NavigationPublicPluginStart; + newsfeed$?: Observable; + solutions: FeatureCatalogueSolution[]; + features: FeatureCatalogueEntry[]; +} + +export const KibanaOverviewApp = ({ + basename, + newsfeed$, + solutions, + features, +}: KibanaOverviewAppDeps) => { + const [newsFetchResult, setNewsFetchResult] = useState(null); + + useEffect(() => { + if (newsfeed$) { + const subscription = newsfeed$.subscribe((res: FetchResult | void | null) => { + setNewsFetchResult(res); + }); + + return () => subscription.unsubscribe(); + } + }, [newsfeed$]); + + return ( + + + + + + + + + + ); +}; diff --git a/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap b/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap new file mode 100644 index 0000000000000..374715a277ebc --- /dev/null +++ b/src/plugins/kibana_overview/public/components/getting_started/__snapshots__/getting_started.test.tsx.snap @@ -0,0 +1,391 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GettingStarted dark mode on 1`] = ` +
+ + +
+ +

+ +

+
+ + +

+ +

+
+ + + + + } + layout="horizontal" + paddingSize="none" + title="Dashboard" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Discover" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Canvas" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Maps" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Machine Learning" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Graph" + titleElement="h3" + titleSize="xs" + /> + + + + + + + + +
+
+ + + +
+
+`; + +exports[`GettingStarted render 1`] = ` +
+ + +
+ +

+ +

+
+ + +

+ +

+
+ + + + + } + layout="horizontal" + paddingSize="none" + title="Dashboard" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Discover" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Canvas" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Maps" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Machine Learning" + titleElement="h3" + titleSize="xs" + /> + + + + } + layout="horizontal" + paddingSize="none" + title="Graph" + titleElement="h3" + titleSize="xs" + /> + + + + + + + + +
+
+ + + +
+
+`; diff --git a/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx b/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx new file mode 100644 index 0000000000000..7d40c4174f39b --- /dev/null +++ b/src/plugins/kibana_overview/public/components/getting_started/getting_started.test.tsx @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { GettingStarted } from './getting_started'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { FeatureCatalogueCategory } from 'src/plugins/home/public'; + +const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); + +const mockApps = [ + { + category: FeatureCatalogueCategory.DATA, + description: 'Display and share a collection of visualizations and saved searches.', + icon: 'dashboardApp', + id: 'dashboard', + order: 100, + path: 'path-to-dashboard', + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Analyze data in dashboards.', + title: 'Dashboard', + }, + { + category: FeatureCatalogueCategory.DATA, + description: 'Interactively explore your data by querying and filtering raw documents.', + icon: 'discoverApp', + id: 'discover', + order: 200, + path: 'path-to-discover', + + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Search and find insights.', + title: 'Discover', + }, + { + category: FeatureCatalogueCategory.DATA, + description: 'Showcase your data in a pixel-perfect way.', + icon: 'canvasApp', + id: 'canvas', + order: 300, + path: 'path-to-canvas', + + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Design pixel-perfect reports.', + title: 'Canvas', + }, + { + category: FeatureCatalogueCategory.DATA, + description: 'Explore geospatial data from Elasticsearch and the Elastic Maps Service.', + icon: 'gisApp', + id: 'maps', + order: 400, + path: 'path-to-maps', + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Plot geographic data.', + title: 'Maps', + }, + { + category: FeatureCatalogueCategory.DATA, + description: + 'Automatically model the normal behavior of your time series data to detect anomalies.', + icon: 'machineLearningApp', + id: 'ml', + order: 500, + path: 'path-to-ml', + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Model, predict, and detect.', + title: 'Machine Learning', + }, + { + category: FeatureCatalogueCategory.DATA, + description: 'Surface and analyze relevant relationships in your Elasticsearch data.', + icon: 'graphApp', + id: 'graph', + order: 600, + path: 'path-to-graph', + showOnHomePage: false, + solutionId: 'kibana', + subtitle: 'Reveal patterns and relationships.', + title: 'Graph', + }, +]; + +describe('GettingStarted', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + test('dark mode on', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx b/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx new file mode 100644 index 0000000000000..9f2d714f43a53 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/getting_started/getting_started.tsx @@ -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 React, { FC } from 'react'; +import { + EuiButton, + EuiCard, + EuiFlexGrid, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiImage, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; +import { PLUGIN_ID } from '../../../common'; + +interface Props { + addBasePath: (path: string) => string; + isDarkTheme: boolean; + apps: FeatureCatalogueEntry[]; +} + +export const GettingStarted: FC = ({ addBasePath, isDarkTheme, apps }) => { + const { + services: { application }, + } = useKibana(); + const gettingStartedGraphicURL = `/plugins/${PLUGIN_ID}/assets/kibana_montage_${ + isDarkTheme ? 'dark' : 'light' + }.svg`; + + return ( +
+ + +
+ +

+ +

+
+ + + + +

+ +

+
+ + + + + {apps.map(({ subtitle = '', icon, title }) => ( + + } + layout="horizontal" + paddingSize="none" + title={title} + titleElement="h3" + titleSize="xs" + /> + + ))} + + + + + + + + + +
+
+ + + + +
+
+ ); +}; diff --git a/src/plugins/data/public/ui/typeahead/index.ts b/src/plugins/kibana_overview/public/components/getting_started/index.ts similarity index 92% rename from src/plugins/data/public/ui/typeahead/index.ts rename to src/plugins/kibana_overview/public/components/getting_started/index.ts index 38b51622b85f7..6a0df12de2d2d 100644 --- a/src/plugins/data/public/ui/typeahead/index.ts +++ b/src/plugins/kibana_overview/public/components/getting_started/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { SuggestionsComponent } from './suggestions_component'; +export * from './getting_started'; diff --git a/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap b/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap new file mode 100644 index 0000000000000..4be9e4df6b736 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/manage_data/__snapshots__/manage_data.test.tsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManageData render 1`] = ` + + +`; + +exports[`ManageData render empty without any features 1`] = ``; diff --git a/src/plugins/kibana_overview/public/components/manage_data/index.tsx b/src/plugins/kibana_overview/public/components/manage_data/index.tsx new file mode 100644 index 0000000000000..2845e3bd12023 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/manage_data/index.tsx @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './manage_data'; diff --git a/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx b/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx new file mode 100644 index 0000000000000..3ce2364c96083 --- /dev/null +++ b/src/plugins/kibana_overview/public/components/manage_data/manage_data.test.tsx @@ -0,0 +1,83 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { ManageData } from './manage_data'; +import { shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { FeatureCatalogueCategory } from 'src/plugins/home/public'; + +const mockFeatures = [ + { + category: FeatureCatalogueCategory.ADMIN, + description: 'Control who has access and what tasks they can perform.', + icon: 'securityApp', + id: 'security', + order: 600, + path: 'path-to-security-roles', + title: 'Protect your data', + showOnHomePage: true, + }, + { + category: FeatureCatalogueCategory.ADMIN, + description: 'Track the real-time health and performance of your deployment.', + icon: 'monitoringApp', + id: 'monitoring', + order: 610, + path: 'path-to-monitoring', + title: 'Monitor the stack', + showOnHomePage: true, + }, + { + category: FeatureCatalogueCategory.ADMIN, + description: + 'Save snapshots to a backup repository, and restore to recover index and cluster state.', + icon: 'storage', + id: 'snapshot_restore', + order: 630, + path: 'path-to-snapshot-restore', + title: 'Store & recover backups', + showOnHomePage: true, + }, + { + category: FeatureCatalogueCategory.ADMIN, + description: 'Define lifecycle policies to automatically perform operations as an index ages.', + icon: 'indexSettings', + id: 'index_lifecycle_management', + order: 640, + path: 'path-to-index-lifecycle-management', + title: 'Manage index lifecycles', + showOnHomePage: true, + }, +]; + +const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); + +describe('ManageData', () => { + test('render', () => { + const component = shallowWithIntl( + + ); + expect(component).toMatchSnapshot(); + }); + + test('render empty without any features', () => { + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx b/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx new file mode 100644 index 0000000000000..f7a40b9370efd --- /dev/null +++ b/src/plugins/kibana_overview/public/components/manage_data/manage_data.tsx @@ -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 React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import { RedirectAppLinks, useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { FeatureCatalogueEntry } from '../../../../../../src/plugins/home/public'; +// @ts-expect-error untyped component +import { Synopsis } from '../synopsis'; + +interface Props { + addBasePath: (path: string) => string; + features: FeatureCatalogueEntry[]; +} + +export const ManageData: FC = ({ addBasePath, features }) => { + const { + services: { application }, + } = useKibana(); + return ( + <> + {features.length > 1 ?
`); + }); +}); diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx new file mode 100644 index 0000000000000..f2eeedb5b7372 --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/components/default_alert.tsx @@ -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 { + EuiButton, + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; +import { MountPoint } from 'kibana/public'; +import React, { useState } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +export const defaultAlertTitle = i18n.translate('security.checkup.insecureClusterTitle', { + defaultMessage: 'Please secure your installation', +}); + +export const defaultAlertText: (onDismiss: (persist: boolean) => void) => MountPoint = ( + onDismiss +) => (e) => { + const AlertText = () => { + const [persist, setPersist] = useState(false); + + return ( + +
+ + + + + setPersist(changeEvent.target.checked)} + label={i18n.translate('security.checkup.dontShowAgain', { + defaultMessage: `Don't show again`, + })} + /> + + + + + {i18n.translate('security.checkup.learnMoreButtonText', { + defaultMessage: `Learn more`, + })} + + + + onDismiss(persist)} + data-test-subj="defaultDismissAlertButton" + > + {i18n.translate('security.checkup.dismissButtonText', { + defaultMessage: `Dismiss`, + })} + + + +
+
+ ); + }; + + render(, e); + + return () => unmountComponentAtNode(e); +}; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts b/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts new file mode 100644 index 0000000000000..9334dad2b8193 --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/components/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { defaultAlertTitle, defaultAlertText } from './default_alert'; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/index.ts b/src/plugins/security_oss/public/insecure_cluster_service/index.ts new file mode 100644 index 0000000000000..7817dc383c168 --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { + InsecureClusterService, + InsecureClusterServiceSetup, + InsecureClusterServiceStart, +} from './insecure_cluster_service'; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx new file mode 100644 index 0000000000000..630becb49dd4c --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.mock.tsx @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { + InsecureClusterServiceSetup, + InsecureClusterServiceStart, +} from './insecure_cluster_service'; + +export const mockInsecureClusterService = { + createSetup: () => { + return { + setAlertTitle: jest.fn(), + setAlertText: jest.fn(), + } as InsecureClusterServiceSetup; + }, + createStart: () => { + return { + hideAlert: jest.fn(), + } as InsecureClusterServiceStart; + }, +}; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx new file mode 100644 index 0000000000000..a81f361689743 --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx @@ -0,0 +1,336 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { InsecureClusterService } from './insecure_cluster_service'; +import { ConfigType } from '../config'; +import { coreMock } from '../../../../core/public/mocks'; +import { nextTick } from 'test_utils/enzyme_helpers'; + +let mockOnDismissCallback: (persist: boolean) => void = jest.fn().mockImplementation(() => { + throw new Error('expected callback to be replaced!'); +}); + +jest.mock('./components', () => { + return { + defaultAlertTitle: 'mocked default alert title', + defaultAlertText: (onDismiss: any) => { + mockOnDismissCallback = onDismiss; + return 'mocked default alert text'; + }, + }; +}); + +interface InitOpts { + displayAlert?: boolean; + isAnonymousPath?: boolean; + tenant?: string; +} + +function initCore({ + displayAlert = true, + isAnonymousPath = false, + tenant = '/server-base-path', +}: InitOpts = {}) { + const coreSetup = coreMock.createSetup(); + (coreSetup.http.basePath.serverBasePath as string) = tenant; + + const coreStart = coreMock.createStart(); + coreStart.http.get.mockImplementation(async (url: unknown) => { + if (url === '/internal/security_oss/display_insecure_cluster_alert') { + return { displayAlert }; + } + throw new Error(`unexpected call to http.get: ${url}`); + }); + coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(isAnonymousPath); + + coreStart.notifications.toasts.addWarning.mockReturnValue({ id: 'mock_alert_id' }); + return { coreSetup, coreStart }; +} + +describe('InsecureClusterService', () => { + describe('display scenarios', () => { + it('does not display an alert when the warning is explicitly disabled via config', async () => { + const config: ConfigType = { showInsecureClusterWarning: false }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).not.toHaveBeenCalled(); + expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('does not display an alert when the endpoint check returns false', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: false }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('does not display an alert when on an anonymous path', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true, isAnonymousPath: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).not.toHaveBeenCalled(); + expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('only reads storage information from the current tenant', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ + displayAlert: true, + tenant: '/my-specific-tenant', + }); + + const storage = coreMock.createStorage(); + storage.getItem.mockReturnValue(JSON.stringify({ show: false })); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(storage.getItem).toHaveBeenCalledTimes(1); + expect(storage.getItem).toHaveBeenCalledWith( + 'insecureClusterWarningVisibility/my-specific-tenant' + ); + }); + + it('does not display an alert when hidden via storage', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + + const storage = coreMock.createStorage(); + storage.getItem.mockReturnValue(JSON.stringify({ show: false })); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).not.toHaveBeenCalled(); + expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('displays an alert when persisted preference is corrupted', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + + const storage = coreMock.createStorage(); + storage.getItem.mockReturnValue('{ this is a string of invalid JSON'); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('displays an alert when enabled via config and endpoint checks', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "iconType": "alert", + "text": "mocked default alert text", + "title": "mocked default alert title", + }, + Object { + "toastLifeTimeMs": 864000000, + }, + ] + `); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + + it('dismisses the alert when requested, and remembers this preference', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + + mockOnDismissCallback(true); + + expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1); + expect(storage.setItem).toHaveBeenCalledWith( + 'insecureClusterWarningVisibility/server-base-path', + JSON.stringify({ show: false }) + ); + }); + }); + + describe('#setup', () => { + it('allows the alert title and text to be replaced exactly once', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const storage = coreMock.createStorage(); + + const { coreSetup } = initCore(); + + const service = new InsecureClusterService(config, storage); + const { setAlertTitle, setAlertText } = service.setup({ core: coreSetup }); + setAlertTitle('some new title'); + setAlertText('some new alert text'); + + expect(() => setAlertTitle('')).toThrowErrorMatchingInlineSnapshot( + `"alert title has already been set"` + ); + expect(() => setAlertText('')).toThrowErrorMatchingInlineSnapshot( + `"alert text has already been set"` + ); + }); + + it('allows the alert title and text to be replaced', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + const { setAlertTitle, setAlertText } = service.setup({ core: coreSetup }); + setAlertTitle('some new title'); + setAlertText('some new alert text'); + + service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "iconType": "alert", + "text": "some new alert text", + "title": "some new title", + }, + Object { + "toastLifeTimeMs": 864000000, + }, + ] + `); + + expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + }); + + describe('#start', () => { + it('allows the alert to be hidden via start contract, and remembers this preference', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + const { hideAlert } = service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + + hideAlert(true); + + expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1); + expect(storage.setItem).toHaveBeenCalledWith( + 'insecureClusterWarningVisibility/server-base-path', + JSON.stringify({ show: false }) + ); + }); + + it('allows the alert to be hidden via start contract, and does not remember the preference', async () => { + const config: ConfigType = { showInsecureClusterWarning: true }; + const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const storage = coreMock.createStorage(); + + const service = new InsecureClusterService(config, storage); + service.setup({ core: coreSetup }); + const { hideAlert } = service.start({ core: coreStart }); + + await nextTick(); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); + + hideAlert(false); + + expect(coreStart.notifications.toasts.remove).toHaveBeenCalledTimes(1); + expect(storage.setItem).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx new file mode 100644 index 0000000000000..e6255233354b7 --- /dev/null +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx @@ -0,0 +1,164 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, MountPoint, Toast } from 'kibana/public'; + +import { BehaviorSubject, combineLatest, from } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { ConfigType } from '../config'; +import { defaultAlertText, defaultAlertTitle } from './components'; + +interface SetupDeps { + core: Pick; +} + +interface StartDeps { + core: Pick; +} + +export interface InsecureClusterServiceSetup { + setAlertTitle: (alertTitle: string | MountPoint) => void; + setAlertText: (alertText: string | MountPoint) => void; +} + +export interface InsecureClusterServiceStart { + hideAlert: (persist: boolean) => void; +} + +export class InsecureClusterService { + private enabled: boolean; + + private alertVisibility$: BehaviorSubject; + + private storage: Storage; + + private alertToast?: Toast; + + private alertTitle?: string | MountPoint; + + private alertText?: string | MountPoint; + + private storageKey?: string; + + constructor(config: Pick, storage: Storage) { + this.storage = storage; + this.enabled = config.showInsecureClusterWarning; + this.alertVisibility$ = new BehaviorSubject(this.enabled); + } + + public setup({ core }: SetupDeps): InsecureClusterServiceSetup { + const tenant = core.http.basePath.serverBasePath; + this.storageKey = `insecureClusterWarningVisibility${tenant}`; + this.enabled = this.enabled && this.getPersistedVisibilityPreference(); + this.alertVisibility$.next(this.enabled); + + return { + setAlertTitle: (alertTitle: string | MountPoint) => { + if (this.alertTitle) { + throw new Error('alert title has already been set'); + } + this.alertTitle = alertTitle; + }, + setAlertText: (alertText: string | MountPoint) => { + if (this.alertText) { + throw new Error('alert text has already been set'); + } + this.alertText = alertText; + }, + }; + } + + public start({ core }: StartDeps): InsecureClusterServiceStart { + const shouldInitialize = + this.enabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname); + + if (shouldInitialize) { + this.initializeAlert(core); + } + + return { + hideAlert: (persist: boolean) => this.setAlertVisibility(false, persist), + }; + } + + private initializeAlert(core: StartDeps['core']) { + const displayAlert$ = from( + core.http + .get<{ displayAlert: boolean }>('/internal/security_oss/display_insecure_cluster_alert') + .catch((e) => { + // in the event we can't make this call, assume we shouldn't display this alert. + return { displayAlert: false }; + }) + ); + + // 10 days is reasonably long enough to call "forever" for a page load. + // Can't go too much longer than this. See https://github.com/elastic/kibana/issues/64264#issuecomment-618400354 + const oneMinute = 60000; + const tenDays = oneMinute * 60 * 24 * 10; + + combineLatest([displayAlert$, this.alertVisibility$]) + .pipe( + map(([{ displayAlert }, isAlertVisible]) => displayAlert && isAlertVisible), + distinctUntilChanged() + ) + .subscribe((showAlert) => { + if (showAlert && !this.alertToast) { + this.alertToast = core.notifications.toasts.addWarning( + { + title: this.alertTitle ?? defaultAlertTitle, + text: + this.alertText ?? + defaultAlertText((persist: boolean) => this.setAlertVisibility(false, persist)), + iconType: 'alert', + }, + { + toastLifeTimeMs: tenDays, + } + ); + } else if (!showAlert && this.alertToast) { + core.notifications.toasts.remove(this.alertToast); + this.alertToast = undefined; + } + }); + } + + private setAlertVisibility(show: boolean, persist: boolean) { + if (!this.enabled) { + return; + } + this.alertVisibility$.next(show); + if (persist) { + this.setPersistedVisibilityPreference(show); + } + } + + private getPersistedVisibilityPreference() { + const entry = this.storage.getItem(this.storageKey!) ?? '{}'; + try { + const { show = true } = JSON.parse(entry); + return show; + } catch (e) { + return true; + } + } + + private setPersistedVisibilityPreference(show: boolean) { + this.storage.setItem(this.storageKey!, JSON.stringify({ show })); + } +} diff --git a/src/plugins/security_oss/public/mocks.ts b/src/plugins/security_oss/public/mocks.ts new file mode 100644 index 0000000000000..f4913d2de671b --- /dev/null +++ b/src/plugins/security_oss/public/mocks.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { mockSecurityOssPlugin } from './plugin.mock'; diff --git a/src/plugins/security_oss/public/plugin.mock.ts b/src/plugins/security_oss/public/plugin.mock.ts new file mode 100644 index 0000000000000..c513d241dccbb --- /dev/null +++ b/src/plugins/security_oss/public/plugin.mock.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mockInsecureClusterService } from './insecure_cluster_service/insecure_cluster_service.mock'; +import { SecurityOssPluginSetup, SecurityOssPluginStart } from './plugin'; + +export const mockSecurityOssPlugin = { + createSetup: () => { + return { + insecureCluster: mockInsecureClusterService.createSetup(), + } as DeeplyMockedKeys; + }, + createStart: () => { + return { + insecureCluster: mockInsecureClusterService.createStart(), + } as DeeplyMockedKeys; + }, +}; diff --git a/src/plugins/security_oss/public/plugin.ts b/src/plugins/security_oss/public/plugin.ts new file mode 100644 index 0000000000000..2f3eed0bde5eb --- /dev/null +++ b/src/plugins/security_oss/public/plugin.ts @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { ConfigType } from './config'; +import { + InsecureClusterService, + InsecureClusterServiceSetup, + InsecureClusterServiceStart, +} from './insecure_cluster_service'; + +export interface SecurityOssPluginSetup { + insecureCluster: InsecureClusterServiceSetup; +} + +export interface SecurityOssPluginStart { + insecureCluster: InsecureClusterServiceStart; +} + +export class SecurityOssPlugin + implements Plugin { + private readonly config: ConfigType; + + private insecureClusterService: InsecureClusterService; + + constructor(private readonly initializerContext: PluginInitializerContext) { + this.config = this.initializerContext.config.get(); + this.insecureClusterService = new InsecureClusterService(this.config, localStorage); + } + + public setup(core: CoreSetup) { + return { + insecureCluster: this.insecureClusterService.setup({ core }), + }; + } + + public start(core: CoreStart) { + return { + insecureCluster: this.insecureClusterService.start({ core }), + }; + } +} diff --git a/src/plugins/security_oss/server/check_cluster_data.test.ts b/src/plugins/security_oss/server/check_cluster_data.test.ts new file mode 100644 index 0000000000000..a8245931daf04 --- /dev/null +++ b/src/plugins/security_oss/server/check_cluster_data.test.ts @@ -0,0 +1,148 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { elasticsearchServiceMock, loggingSystemMock } from '../../../core/server/mocks'; +import { createClusterDataCheck } from './check_cluster_data'; + +describe('checkClusterForUserData', () => { + it('returns false if no data is found', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.cat.indices.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ body: [] }) + ); + + const log = loggingSystemMock.createLogger(); + + const response = await createClusterDataCheck()(esClient, log); + expect(response).toEqual(false); + expect(esClient.cat.indices).toHaveBeenCalledTimes(1); + }); + + it('returns false if data only exists in system indices', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.cat.indices.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: [ + { + index: '.kibana', + 'docs.count': 500, + }, + { + index: 'kibana_sample_ecommerce_data', + 'docs.count': 20, + }, + { + index: '.somethingElse', + 'docs.count': 20, + }, + ], + }) + ); + + const log = loggingSystemMock.createLogger(); + + const response = await createClusterDataCheck()(esClient, log); + expect(response).toEqual(false); + expect(esClient.cat.indices).toHaveBeenCalledTimes(1); + }); + + it('returns true if data exists in non-system indices', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.cat.indices.mockResolvedValue( + elasticsearchServiceMock.createApiResponse({ + body: [ + { + index: '.kibana', + 'docs.count': 500, + }, + { + index: 'some_real_index', + 'docs.count': 20, + }, + ], + }) + ); + + const log = loggingSystemMock.createLogger(); + + const response = await createClusterDataCheck()(esClient, log); + expect(response).toEqual(true); + }); + + it('checks each time until the first true response is returned, then stops checking', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); + esClient.cat.indices + .mockResolvedValueOnce( + elasticsearchServiceMock.createApiResponse({ + body: [], + }) + ) + .mockRejectedValueOnce(new Error('something terrible happened')) + .mockResolvedValueOnce( + elasticsearchServiceMock.createApiResponse({ + body: [ + { + index: '.kibana', + 'docs.count': 500, + }, + ], + }) + ) + .mockResolvedValueOnce( + elasticsearchServiceMock.createApiResponse({ + body: [ + { + index: 'some_real_index', + 'docs.count': 20, + }, + ], + }) + ); + + const log = loggingSystemMock.createLogger(); + + const doesClusterHaveUserData = createClusterDataCheck(); + + let response = await doesClusterHaveUserData(esClient, log); + expect(response).toEqual(false); + + response = await doesClusterHaveUserData(esClient, log); + expect(response).toEqual(false); + + response = await doesClusterHaveUserData(esClient, log); + expect(response).toEqual(false); + + response = await doesClusterHaveUserData(esClient, log); + expect(response).toEqual(true); + + expect(esClient.cat.indices).toHaveBeenCalledTimes(4); + expect(log.warn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "Error encountered while checking cluster for user data: Error: something terrible happened", + ], + ] + `); + + response = await doesClusterHaveUserData(esClient, log); + expect(response).toEqual(true); + // Same number of calls as above. We should not have to interrogate again. + expect(esClient.cat.indices).toHaveBeenCalledTimes(4); + }); +}); diff --git a/src/plugins/security_oss/server/check_cluster_data.ts b/src/plugins/security_oss/server/check_cluster_data.ts new file mode 100644 index 0000000000000..a3aeb50ae280a --- /dev/null +++ b/src/plugins/security_oss/server/check_cluster_data.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ElasticsearchClient, Logger } from 'kibana/server'; + +export const createClusterDataCheck = () => { + let clusterHasUserData = false; + + return async function doesClusterHaveUserData(esClient: ElasticsearchClient, log: Logger) { + if (!clusterHasUserData) { + try { + const indices = await esClient.cat.indices< + Array<{ index: string; ['docs.count']: string }> + >({ + format: 'json', + h: ['index', 'docs.count'], + }); + clusterHasUserData = indices.body.some((indexCount) => { + const isInternalIndex = + indexCount.index.startsWith('.') || indexCount.index.startsWith('kibana_sample_'); + + return !isInternalIndex && parseInt(indexCount['docs.count'], 10) > 0; + }); + } catch (e) { + log.warn(`Error encountered while checking cluster for user data: ${e}`); + clusterHasUserData = false; + } + } + return clusterHasUserData; + }; +}; diff --git a/src/plugins/security_oss/server/config.ts b/src/plugins/security_oss/server/config.ts new file mode 100644 index 0000000000000..17fb46065aee5 --- /dev/null +++ b/src/plugins/security_oss/server/config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export type ConfigType = TypeOf; + +export const ConfigSchema = schema.object({ + showInsecureClusterWarning: schema.boolean({ defaultValue: true }), +}); diff --git a/src/plugins/security_oss/server/index.ts b/src/plugins/security_oss/server/index.ts new file mode 100644 index 0000000000000..f35ae39daaff3 --- /dev/null +++ b/src/plugins/security_oss/server/index.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; +import { ConfigSchema } from './config'; +import { SecurityOssPlugin } from './plugin'; + +export { SecurityOssPluginSetup } from './plugin'; + +export const config: PluginConfigDescriptor> = { + schema: ConfigSchema, + exposeToBrowser: { + showInsecureClusterWarning: true, + }, +}; + +export const plugin = (context: PluginInitializerContext) => new SecurityOssPlugin(context); diff --git a/src/plugins/security_oss/server/plugin.test.ts b/src/plugins/security_oss/server/plugin.test.ts new file mode 100644 index 0000000000000..417da0c7e73bb --- /dev/null +++ b/src/plugins/security_oss/server/plugin.test.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { coreMock } from '../../../core/server/mocks'; +import { SecurityOssPlugin } from './plugin'; + +describe('SecurityOss Plugin', () => { + describe('#setup', () => { + it('exposes the proper contract', async () => { + const context = coreMock.createPluginInitializerContext(); + const plugin = new SecurityOssPlugin(context); + const core = coreMock.createSetup(); + const contract = plugin.setup(core); + expect(Object.keys(contract)).toMatchInlineSnapshot(` + Array [ + "showInsecureClusterWarning$", + ] + `); + }); + }); +}); diff --git a/src/plugins/security_oss/server/plugin.ts b/src/plugins/security_oss/server/plugin.ts new file mode 100644 index 0000000000000..e48827f21a13a --- /dev/null +++ b/src/plugins/security_oss/server/plugin.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { createClusterDataCheck } from './check_cluster_data'; +import { ConfigType } from './config'; +import { setupDisplayInsecureClusterAlertRoute } from './routes'; + +export interface SecurityOssPluginSetup { + /** + * Allows consumers to show/hide the insecure cluster warning. + */ + showInsecureClusterWarning$: BehaviorSubject; +} + +export class SecurityOssPlugin implements Plugin { + private readonly config$: Observable; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.config$ = initializerContext.config.create(); + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + const router = core.http.createRouter(); + const showInsecureClusterWarning$ = new BehaviorSubject(true); + + setupDisplayInsecureClusterAlertRoute({ + router, + log: this.logger, + config$: this.config$, + displayModifier$: showInsecureClusterWarning$, + doesClusterHaveUserData: createClusterDataCheck(), + }); + + return { + showInsecureClusterWarning$, + }; + } + + public start() {} + + public stop() {} +} diff --git a/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts b/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts new file mode 100644 index 0000000000000..0f0f72f054b4c --- /dev/null +++ b/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.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 { IRouter, Logger } from 'kibana/server'; +import { combineLatest, Observable } from 'rxjs'; +import { createClusterDataCheck } from '../check_cluster_data'; +import { ConfigType } from '../config'; + +interface Deps { + router: IRouter; + log: Logger; + config$: Observable; + displayModifier$: Observable; + doesClusterHaveUserData: ReturnType; +} + +export const setupDisplayInsecureClusterAlertRoute = ({ + router, + log, + config$, + displayModifier$, + doesClusterHaveUserData, +}: Deps) => { + let showInsecureClusterWarning = false; + + combineLatest([config$, displayModifier$]).subscribe(([config, displayModifier]) => { + showInsecureClusterWarning = config.showInsecureClusterWarning && displayModifier; + }); + + router.get( + { + path: '/internal/security_oss/display_insecure_cluster_alert', + validate: false, + }, + async (context, request, response) => { + if (!showInsecureClusterWarning) { + return response.ok({ body: { displayAlert: false } }); + } + + const hasData = await doesClusterHaveUserData( + context.core.elasticsearch.client.asInternalUser, + log + ); + return response.ok({ body: { displayAlert: hasData } }); + } + ); +}; diff --git a/src/plugins/security_oss/server/routes/index.ts b/src/plugins/security_oss/server/routes/index.ts new file mode 100644 index 0000000000000..ceff0b12c9cb1 --- /dev/null +++ b/src/plugins/security_oss/server/routes/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { setupDisplayInsecureClusterAlertRoute } from './display_insecure_cluster_alert'; diff --git a/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts b/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts new file mode 100644 index 0000000000000..d62a5040be6b3 --- /dev/null +++ b/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.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 { loggingSystemMock } from '../../../../../core/server/mocks'; +import { setupServer } from '../../../../../core/server/test_utils'; +import { setupDisplayInsecureClusterAlertRoute } from '../display_insecure_cluster_alert'; +import { ConfigType } from '../../config'; +import { BehaviorSubject, of } from 'rxjs'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { createClusterDataCheck } from '../../check_cluster_data'; +import supertest from 'supertest'; + +type SetupServerReturn = UnwrapPromise>; +const pluginId = Symbol('securityOss'); + +interface SetupOpts { + config?: ConfigType; + displayModifier$?: BehaviorSubject; + doesClusterHaveUserData?: ReturnType; +} + +describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + + const setupTestServer = async ({ + config = { showInsecureClusterWarning: true }, + displayModifier$ = new BehaviorSubject(true), + doesClusterHaveUserData = jest.fn().mockResolvedValue(true), + }: SetupOpts) => { + ({ server, httpSetup } = await setupServer(pluginId)); + + const router = httpSetup.createRouter('/'); + const log = loggingSystemMock.createLogger(); + + setupDisplayInsecureClusterAlertRoute({ + router, + log, + config$: of(config), + displayModifier$, + doesClusterHaveUserData, + }); + + await server.start(); + + return { + log, + }; + }; + + afterEach(async () => { + await server.stop(); + }); + + it('responds `false` if plugin is not configured to display alerts', async () => { + await setupTestServer({ + config: { showInsecureClusterWarning: false }, + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: false }); + }); + + it('responds `false` if cluster does not contain user data', async () => { + await setupTestServer({ + config: { showInsecureClusterWarning: true }, + doesClusterHaveUserData: jest.fn().mockResolvedValue(false), + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: false }); + }); + + it('responds `false` if displayModifier$ is set to false', async () => { + await setupTestServer({ + config: { showInsecureClusterWarning: true }, + doesClusterHaveUserData: jest.fn().mockResolvedValue(true), + displayModifier$: new BehaviorSubject(false), + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: false }); + }); + + it('responds `true` if cluster contains user data', async () => { + await setupTestServer({ + config: { showInsecureClusterWarning: true }, + doesClusterHaveUserData: jest.fn().mockResolvedValue(true), + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: true }); + }); + + it('responds to changing displayModifier$ values', async () => { + const displayModifier$ = new BehaviorSubject(true); + + await setupTestServer({ + config: { showInsecureClusterWarning: true }, + doesClusterHaveUserData: jest.fn().mockResolvedValue(true), + displayModifier$, + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: true }); + + displayModifier$.next(false); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/display_insecure_cluster_alert') + .expect(200, { displayAlert: false }); + }); +}); diff --git a/src/plugins/share/public/index.ts b/src/plugins/share/public/index.ts index e3d6c41a278cd..950ecebeaadc7 100644 --- a/src/plugins/share/public/index.ts +++ b/src/plugins/share/public/index.ts @@ -40,4 +40,6 @@ export { import { SharePlugin } from './plugin'; +export { KibanaURL } from './kibana_url'; + export const plugin = () => new SharePlugin(); diff --git a/src/plugins/share/public/kibana_url.ts b/src/plugins/share/public/kibana_url.ts new file mode 100644 index 0000000000000..40c3372579f6a --- /dev/null +++ b/src/plugins/share/public/kibana_url.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. + */ + +// TODO: Replace this logic with KibanaURL once it is available. +// https://github.com/elastic/kibana/issues/64497 +export class KibanaURL { + public readonly path: string; + public readonly appName: string; + public readonly appPath: string; + + constructor(path: string) { + const match = path.match(/^.*\/app\/([^\/#]+)(.+)$/); + + if (!match) { + throw new Error('Unexpected URL path.'); + } + + const [, appName, appPath] = match; + + if (!appName || !appPath) { + throw new Error('Could not parse URL path.'); + } + + this.path = path; + this.appName = appName; + this.appPath = appPath; + } +} diff --git a/src/plugins/share/public/types.ts b/src/plugins/share/public/types.ts index 9dcfc3d9e8143..19f33a820a11a 100644 --- a/src/plugins/share/public/types.ts +++ b/src/plugins/share/public/types.ts @@ -18,7 +18,8 @@ */ import { ComponentType } from 'react'; -import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiContextMenuPanelItemDescriptorEntry } from '@elastic/eui/src/components/context_menu/context_menu'; /** * @public @@ -53,7 +54,8 @@ export interface ShareContext { * used to order the individual items in a flat list returned by all registered * menu providers. * */ -export interface ShareContextMenuPanelItem extends Omit { +export interface ShareContextMenuPanelItem + extends Omit { name: string; // EUI will accept a `ReactNode` for the `name` prop, but `ShareContentMenu` assumes a `string`. sortOrder?: number; } diff --git a/src/plugins/telemetry/server/fetcher.test.ts b/src/plugins/telemetry/server/fetcher.test.ts index 245adf59799cc..45712df772e1c 100644 --- a/src/plugins/telemetry/server/fetcher.test.ts +++ b/src/plugins/telemetry/server/fetcher.test.ts @@ -23,19 +23,93 @@ import { coreMock } from '../../../core/server/mocks'; describe('FetcherTask', () => { describe('sendIfDue', () => { - it('returns undefined and warns when it fails to get telemetry configs', async () => { + it('stops when it fails to get telemetry configs', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); const fetcherTask = new FetcherTask(initializerContext); const mockError = new Error('Some message.'); - fetcherTask['getCurrentConfigs'] = async () => { - throw mockError; - }; + const getCurrentConfigs = jest.fn().mockRejectedValue(mockError); + const fetchTelemetry = jest.fn(); + const sendTelemetry = jest.fn(); + Object.assign(fetcherTask, { + getCurrentConfigs, + fetchTelemetry, + sendTelemetry, + }); const result = await fetcherTask['sendIfDue'](); expect(result).toBe(undefined); + expect(getCurrentConfigs).toBeCalledTimes(1); + expect(fetchTelemetry).toBeCalledTimes(0); + expect(sendTelemetry).toBeCalledTimes(0); expect(fetcherTask['logger'].warn).toBeCalledTimes(1); expect(fetcherTask['logger'].warn).toHaveBeenCalledWith( - `Error fetching telemetry configs: ${mockError}` + `Error getting telemetry configs. (${mockError})` ); }); + + it('stops when all collectors are not ready', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const fetcherTask = new FetcherTask(initializerContext); + const getCurrentConfigs = jest.fn().mockResolvedValue({}); + const areAllCollectorsReady = jest.fn().mockResolvedValue(false); + const shouldSendReport = jest.fn().mockReturnValue(true); + const fetchTelemetry = jest.fn(); + const sendTelemetry = jest.fn(); + const updateReportFailure = jest.fn(); + + Object.assign(fetcherTask, { + getCurrentConfigs, + areAllCollectorsReady, + shouldSendReport, + fetchTelemetry, + updateReportFailure, + sendTelemetry, + }); + + await fetcherTask['sendIfDue'](); + + expect(fetchTelemetry).toBeCalledTimes(0); + expect(sendTelemetry).toBeCalledTimes(0); + + expect(areAllCollectorsReady).toBeCalledTimes(1); + expect(updateReportFailure).toBeCalledTimes(0); + expect(fetcherTask['logger'].warn).toBeCalledTimes(1); + expect(fetcherTask['logger'].warn).toHaveBeenCalledWith( + `Error fetching usage. (Error: Not all collectors are ready.)` + ); + }); + + it('fetches usage and send telemetry', async () => { + const initializerContext = coreMock.createPluginInitializerContext({}); + const fetcherTask = new FetcherTask(initializerContext); + const mockTelemetryUrl = 'mock_telemetry_url'; + const mockClusters = ['cluster_1', 'cluster_2']; + const getCurrentConfigs = jest.fn().mockResolvedValue({ + telemetryUrl: mockTelemetryUrl, + }); + const areAllCollectorsReady = jest.fn().mockResolvedValue(true); + const shouldSendReport = jest.fn().mockReturnValue(true); + + const fetchTelemetry = jest.fn().mockResolvedValue(mockClusters); + const sendTelemetry = jest.fn(); + const updateReportFailure = jest.fn(); + + Object.assign(fetcherTask, { + getCurrentConfigs, + areAllCollectorsReady, + shouldSendReport, + fetchTelemetry, + updateReportFailure, + sendTelemetry, + }); + + await fetcherTask['sendIfDue'](); + + expect(areAllCollectorsReady).toBeCalledTimes(1); + expect(fetchTelemetry).toBeCalledTimes(1); + expect(sendTelemetry).toBeCalledTimes(2); + expect(sendTelemetry).toHaveBeenNthCalledWith(1, mockTelemetryUrl, mockClusters[0]); + expect(sendTelemetry).toHaveBeenNthCalledWith(2, mockTelemetryUrl, mockClusters[1]); + expect(updateReportFailure).toBeCalledTimes(0); + }); }); }); diff --git a/src/plugins/telemetry/server/fetcher.ts b/src/plugins/telemetry/server/fetcher.ts index 75cfac721bcd3..fadfc01f628f5 100644 --- a/src/plugins/telemetry/server/fetcher.ts +++ b/src/plugins/telemetry/server/fetcher.ts @@ -18,11 +18,14 @@ */ import moment from 'moment'; -import { Observable } from 'rxjs'; +import { Observable, Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; // @ts-ignore import fetch from 'node-fetch'; -import { TelemetryCollectionManagerPluginStart } from 'src/plugins/telemetry_collection_manager/server'; +import { + TelemetryCollectionManagerPluginStart, + UsageStatsPayload, +} from 'src/plugins/telemetry_collection_manager/server'; import { PluginInitializerContext, Logger, @@ -58,7 +61,7 @@ export class FetcherTask { private readonly config$: Observable; private readonly currentKibanaVersion: string; private readonly logger: Logger; - private intervalId?: NodeJS.Timeout; + private intervalId?: Subscription; private lastReported?: number; private isSending = false; private internalRepository?: SavedObjectsClientContract; @@ -79,21 +82,24 @@ export class FetcherTask { this.telemetryCollectionManager = telemetryCollectionManager; this.elasticsearchClient = elasticsearch.legacy.createClient('telemetry-fetcher'); - setTimeout(() => { - this.sendIfDue(); - this.intervalId = setInterval(() => this.sendIfDue(), this.checkIntervalMs); - }, this.initialCheckDelayMs); + this.intervalId = timer(this.initialCheckDelayMs, this.checkIntervalMs).subscribe(() => + this.sendIfDue() + ); } public stop() { if (this.intervalId) { - clearInterval(this.intervalId); + this.intervalId.unsubscribe(); } if (this.elasticsearchClient) { this.elasticsearchClient.close(); } } + private async areAllCollectorsReady() { + return (await this.telemetryCollectionManager?.areAllCollectorsReady()) ?? false; + } + private async sendIfDue() { if (this.isSending) { return; @@ -103,7 +109,7 @@ export class FetcherTask { try { telemetryConfig = await this.getCurrentConfigs(); } catch (err) { - this.logger.warn(`Error fetching telemetry configs: ${err}`); + this.logger.warn(`Error getting telemetry configs. (${err})`); return; } @@ -111,9 +117,22 @@ export class FetcherTask { return; } + let clusters: Array = []; + this.isSending = true; + + try { + const allCollectorsReady = await this.areAllCollectorsReady(); + if (!allCollectorsReady) { + throw new Error('Not all collectors are ready.'); + } + clusters = await this.fetchTelemetry(); + } catch (err) { + this.logger.warn(`Error fetching usage. (${err})`); + this.isSending = false; + return; + } + try { - this.isSending = true; - const clusters = await this.fetchTelemetry(); const { telemetryUrl } = telemetryConfig; for (const cluster of clusters) { await this.sendTelemetry(telemetryUrl, cluster); @@ -123,7 +142,7 @@ export class FetcherTask { } catch (err) { await this.updateReportFailure(telemetryConfig); - this.logger.warn(`Error sending telemetry usage data: ${err}`); + this.logger.warn(`Error sending telemetry usage data. (${err})`); } this.isSending = false; } diff --git a/src/plugins/telemetry/server/plugin.ts b/src/plugins/telemetry/server/plugin.ts index dfbbe3355e69c..b423cbb07ba32 100644 --- a/src/plugins/telemetry/server/plugin.ts +++ b/src/plugins/telemetry/server/plugin.ts @@ -18,7 +18,7 @@ */ import { URL } from 'url'; -import { Observable } from 'rxjs'; +import { AsyncSubject, Observable } from 'rxjs'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { TelemetryCollectionManagerPluginSetup, @@ -30,11 +30,11 @@ import { PluginInitializerContext, ISavedObjectsRepository, CoreStart, - IUiSettingsClient, SavedObjectsClient, Plugin, Logger, IClusterClient, + UiSettingsServiceStart, } from '../../../core/server'; import { registerRoutes } from './routes'; import { registerCollection } from './telemetry_collection'; @@ -82,8 +82,11 @@ export class TelemetryPlugin implements Plugin; private readonly isDev: boolean; private readonly fetcherTask: FetcherTask; + /** + * @private Used to mark the completion of the old UI Settings migration + */ + private readonly oldUiSettingsHandled$ = new AsyncSubject(); private savedObjectsClient?: ISavedObjectsRepository; - private uiSettingsClient?: IUiSettingsClient; private elasticsearchClient?: IClusterClient; constructor(initializerContext: PluginInitializerContext) { @@ -97,10 +100,10 @@ export class TelemetryPlugin implements Plugin { + ): TelemetryPluginSetup { const currentKibanaVersion = this.currentKibanaVersion; const config$ = this.config$; const isDev = this.isDev; @@ -131,25 +134,21 @@ export class TelemetryPlugin implements Plugin { - const internalRepository = new SavedObjectsClient(savedObjects.createInternalRepository()); - const telemetrySavedObject = await getTelemetrySavedObject(internalRepository!); + await this.oldUiSettingsHandled$.pipe(take(1)).toPromise(); // Wait for the old settings to be handled + const internalRepository = new SavedObjectsClient(savedObjectsInternalRepository); + const telemetrySavedObject = await getTelemetrySavedObject(internalRepository); const config = await this.config$.pipe(take(1)).toPromise(); const allowChangingOptInStatus = config.allowChangingOptInStatus; const configTelemetryOptIn = typeof config.optIn === 'undefined' ? null : config.optIn; @@ -166,6 +165,27 @@ export class TelemetryPlugin implements Plugin { + return await this.usageCollection?.areAllCollectorsReady(); + }; + private getOptInStatsForCollection = async ( collection: Collection, optInStatus: boolean, diff --git a/src/plugins/telemetry_collection_manager/server/types.ts b/src/plugins/telemetry_collection_manager/server/types.ts index 44970df30fd16..3b0936fb73a60 100644 --- a/src/plugins/telemetry_collection_manager/server/types.ts +++ b/src/plugins/telemetry_collection_manager/server/types.ts @@ -34,6 +34,7 @@ export interface TelemetryCollectionManagerPluginSetup { ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; + areAllCollectorsReady: TelemetryCollectionManagerPlugin['areAllCollectorsReady']; } export interface TelemetryCollectionManagerPluginStart { @@ -42,6 +43,7 @@ export interface TelemetryCollectionManagerPluginStart { ) => void; getOptInStats: TelemetryCollectionManagerPlugin['getOptInStats']; getStats: TelemetryCollectionManagerPlugin['getStats']; + areAllCollectorsReady: TelemetryCollectionManagerPlugin['areAllCollectorsReady']; } export interface TelemetryOptInStats { diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx index f7fb4daff63f0..1a7b11ccf6e20 100644 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -76,7 +76,7 @@ function TileMapOptions(props: TileMapOptionsProps) { (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; // (undocumented) readonly attachAction: (triggerId: T, actionId: string) => void; readonly clear: () => void; @@ -239,21 +246,21 @@ export class UiActionsService { readonly executionService: UiActionsExecutionService; readonly fork: () => UiActionsService; // (undocumented) - readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; + readonly getAction: >(id: string) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) readonly getTrigger: (triggerId: T) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: T) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly registerAction:
>(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; + readonly registerAction: >(definition: A) => Action, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK">; // (undocumented) readonly registerTrigger: (trigger: Trigger) => void; // Warning: (ae-forgotten-export) The symbol "TriggerRegistry" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index b00f4628ffb96..0be3c19fc1c4d 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -57,10 +57,12 @@ export interface TriggerContextMapping { const DEFAULT_ACTION = ''; export const ACTION_VISUALIZE_FIELD = 'ACTION_VISUALIZE_FIELD'; export const ACTION_VISUALIZE_GEO_FIELD = 'ACTION_VISUALIZE_GEO_FIELD'; +export const ACTION_VISUALIZE_LENS_FIELD = 'ACTION_VISUALIZE_LENS_FIELD'; export type ActionType = keyof ActionContextMapping; export interface ActionContextMapping { [DEFAULT_ACTION]: BaseContext; [ACTION_VISUALIZE_FIELD]: VisualizeFieldContext; [ACTION_VISUALIZE_GEO_FIELD]: VisualizeFieldContext; + [ACTION_VISUALIZE_LENS_FIELD]: VisualizeFieldContext; } diff --git a/src/plugins/url_forwarding/public/forward_app/forward_app.ts b/src/plugins/url_forwarding/public/forward_app/forward_app.ts index 967b18769ebc6..0480317a6b720 100644 --- a/src/plugins/url_forwarding/public/forward_app/forward_app.ts +++ b/src/plugins/url_forwarding/public/forward_app/forward_app.ts @@ -29,6 +29,7 @@ export const createLegacyUrlForwardApp = ( id: 'kibana', chromeless: true, title: 'Legacy URL migration', + appRoute: '/app/kibana#/', navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { const hash = params.history.location.hash.substr(1); diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index 9955f9fac81ca..aae633a956c48 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -325,3 +325,8 @@ By storing these metrics and their counts as key-value pairs, we can add more me to worry about exceeding the 1000-field soft limit in Elasticsearch. The only caveat is that it makes it harder to consume in Kibana when analysing each entry in the array separately. In the telemetry team we are working to find a solution to this. + +# Routes registered by this plugin + +- `/api/ui_metric/report`: Used by `ui_metrics` usage collector instances to report their usage data to the server +- `/api/stats`: Get the metrics and usage ([details](./server/routes/stats/README.md)) diff --git a/src/plugins/usage_collection/server/collector/collector_set.ts b/src/plugins/usage_collection/server/collector/collector_set.ts index 6861be7f4f76b..7bf4e19c72cc0 100644 --- a/src/plugins/usage_collection/server/collector/collector_set.ts +++ b/src/plugins/usage_collection/server/collector/collector_set.ts @@ -76,23 +76,27 @@ export class CollectorSet { }; public areAllCollectorsReady = async (collectorSet: CollectorSet = this) => { - // Kept this for runtime validation in JS code. if (!(collectorSet instanceof CollectorSet)) { throw new Error( `areAllCollectorsReady method given bad collectorSet parameter: ` + typeof collectorSet ); } - const collectorTypesNotReady = ( - await Promise.all( - [...collectorSet.collectors.values()].map(async (collector) => { - if (!(await collector.isReady())) { - return collector.type; - } - }) - ) - ).filter((collectorType): collectorType is string => !!collectorType); - const allReady = collectorTypesNotReady.length === 0; + const collectors = [...collectorSet.collectors.values()]; + const collectorsWithStatus = await Promise.all( + collectors.map(async (collector) => { + return { + isReady: await collector.isReady(), + collector, + }; + }) + ); + + const collectorsTypesNotReady = collectorsWithStatus + .filter((collectorWithStatus) => collectorWithStatus.isReady === false) + .map((collectorWithStatus) => collectorWithStatus.collector.type); + + const allReady = collectorsTypesNotReady.length === 0; if (!allReady && this.maximumWaitTimeForAllCollectorsInS >= 0) { const nowTimestamp = +new Date(); @@ -102,10 +106,11 @@ export class CollectorSet { const timeLeftInMS = this.maximumWaitTimeForAllCollectorsInS * 1000 - timeWaitedInMS; if (timeLeftInMS <= 0) { this.logger.debug( - `All collectors are not ready (waiting for ${collectorTypesNotReady.join(',')}) ` + + `All collectors are not ready (waiting for ${collectorsTypesNotReady.join(',')}) ` + `but we have waited the required ` + `${this.maximumWaitTimeForAllCollectorsInS}s and will return data from all collectors that are ready.` ); + return true; } else { this.logger.debug(`All collectors are not ready. Waiting for ${timeLeftInMS}ms longer.`); diff --git a/src/plugins/usage_collection/server/routes/stats/README.md b/src/plugins/usage_collection/server/routes/stats/README.md new file mode 100644 index 0000000000000..09dabefbab44a --- /dev/null +++ b/src/plugins/usage_collection/server/routes/stats/README.md @@ -0,0 +1,20 @@ +# `/api/stats` + +This API returns the metrics for the Kibana server and usage stats. It allows the [Metricbeat Kibana module](https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-module-kibana.html) to collect the [stats metricset](https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-metricset-kibana-stats.html). + +By default, it returns the simplest level of stats; consisting of the Kibana server's ops metrics, version, status, and basic config like the server name, host, port, and locale. + +However, the information detailed above can be extended, with the combination of the following 3 query parameters: + +| Query Parameter | Default value | Description | +|:----------------|:-------------:|:------------| +|`extended`|`false`|When `true`, it adds `clusterUuid` and `usage`. The latter contains the information reported by all the Usage Collectors registered in the Kibana server. It may throw `503 Stats not ready` if any of the collectors is not fully initialized yet.| +|`legacy`|`false`|By default, when `extended=true`, the key names of the data in `usage` are transformed into API-friendlier `snake_case` format (i.e.: `clusterUuid` is transformed to `cluster_uuid`). When this parameter is `true`, the data is returned as-is.| +|`exclude_usage`|`false`|When `true`, and `extended=true`, it will report `clusterUuid` but no `usage`.| + +## Known use cases + +Metricbeat Kibana' stats metricset ([code](https://github.com/elastic/beats/blob/master/metricbeat/module/kibana/stats/stats.go)) uses this API to collect the metrics (every 10s) and usage (only once every 24h), and then reports them to the Monitoring cluster. They call this API in 2 ways: + +1. Metrics-only collection (every 10 seconds): `GET /api/stats?extended=true&legacy=true&exclude_usage=true` +2. Metrics+usage (every 24 hours): `GET /api/stats?extended=true&legacy=true&exclude_usage=false` diff --git a/src/plugins/usage_collection/server/routes/stats/index.ts b/src/plugins/usage_collection/server/routes/stats/index.ts new file mode 100644 index 0000000000000..8871ee599e56b --- /dev/null +++ b/src/plugins/usage_collection/server/routes/stats/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { registerStatsRoute } from './stats'; diff --git a/src/plugins/usage_collection/server/routes/stats.ts b/src/plugins/usage_collection/server/routes/stats/stats.ts similarity index 91% rename from src/plugins/usage_collection/server/routes/stats.ts rename to src/plugins/usage_collection/server/routes/stats/stats.ts index ef5da2eb11ba6..bee25fef669f1 100644 --- a/src/plugins/usage_collection/server/routes/stats.ts +++ b/src/plugins/usage_collection/server/routes/stats/stats.ts @@ -30,8 +30,8 @@ import { MetricsServiceSetup, ServiceStatus, ServiceStatusLevels, -} from '../../../../core/server'; -import { CollectorSet } from '../collector'; +} from '../../../../../core/server'; +import { CollectorSet } from '../../collector'; const STATS_NOT_READY_MESSAGE = i18n.translate('usageCollection.stats.notReadyMessage', { defaultMessage: 'Stats are not ready yet. Please try again later.', @@ -101,10 +101,12 @@ export function registerStatsRoute({ if (isExtended) { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const esClient = context.core.elasticsearch.client.asCurrentUser; - const collectorsReady = await collectorSet.areAllCollectorsReady(); - if (shouldGetUsage && !collectorsReady) { - return res.customError({ statusCode: 503, body: { message: STATS_NOT_READY_MESSAGE } }); + if (shouldGetUsage) { + const collectorsReady = await collectorSet.areAllCollectorsReady(); + if (!collectorsReady) { + return res.customError({ statusCode: 503, body: { message: STATS_NOT_READY_MESSAGE } }); + } } const usagePromise = shouldGetUsage ? getUsage(callCluster, esClient) : Promise.resolve({}); @@ -152,9 +154,8 @@ export function registerStatsRoute({ } } - // Guranteed to resolve immediately due to replay effect on getOpsMetrics$ - // eslint-disable-next-line @typescript-eslint/naming-convention - const { collected_at, ...lastMetrics } = await metrics + // Guaranteed to resolve immediately due to replay effect on getOpsMetrics$ + const { collected_at: collectedAt, ...lastMetrics } = await metrics .getOpsMetrics$() .pipe(first()) .toPromise(); @@ -173,7 +174,7 @@ export function registerStatsRoute({ snapshot: SNAPSHOT_REGEX.test(config.kibanaVersion), status: ServiceStatusToLegacyState[overallStatus.level.toString()], }, - last_updated: collected_at.toISOString(), + last_updated: collectedAt.toISOString(), collection_interval_in_millis: metrics.collectionInterval, }); diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx index c0a6b48794970..d6b69a769e0a3 100644 --- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx +++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx @@ -18,7 +18,7 @@ */ import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react'; -import { get, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { keys, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EventEmitter } from 'events'; @@ -71,7 +71,7 @@ function DefaultEditorSideBar({ ]); const metricSchemas = (vis.type.schemas.metrics || []).map((s: Schema) => s.name); const metricAggs = useMemo( - () => responseAggs.filter((agg) => metricSchemas.includes(get(agg, 'schema'))), + () => responseAggs.filter((agg) => agg.schema && metricSchemas.includes(agg.schema)), [responseAggs, metricSchemas] ); const hasHistogramAgg = useMemo(() => responseAggs.some((agg) => agg.type.name === 'histogram'), [ diff --git a/src/plugins/vis_type_metric/public/to_ast.ts b/src/plugins/vis_type_metric/public/to_ast.ts index 7eefd8328ab76..23e4664b82414 100644 --- a/src/plugins/vis_type_metric/public/to_ast.ts +++ b/src/plugins/vis_type_metric/public/to_ast.ts @@ -39,7 +39,7 @@ export const toExpressionAst = (vis: Vis, params: any) => { const esaggs = buildExpressionFunction('esaggs', { index: vis.data.indexPattern!.id!, metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.type.requiresPartialRows || vis.params.showPartialRows || false, + partialRows: vis.params.showPartialRows || false, aggConfigs: JSON.stringify(vis.data.aggs!.aggs), includeFormatHints: false, }); diff --git a/src/plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts index 2b4017ae0ee81..035ca044137e9 100644 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_controller.test.ts @@ -249,13 +249,13 @@ describe('Table Vis - Controller', () => { const vis = getRangeVis({ showPartialRows: true }); initController(vis); - expect(vis.type.hierarchicalData(vis)).toEqual(true); + expect((vis.type.hierarchicalData as Function)(vis)).toEqual(true); }); test('passes partialRows:false to tabify based on the vis params', () => { const vis = getRangeVis({ showPartialRows: false }); initController(vis); - expect(vis.type.hierarchicalData(vis)).toEqual(false); + expect((vis.type.hierarchicalData as Function)(vis)).toEqual(false); }); }); diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c1419a4847458..95f4f06ee6111 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -20,7 +20,7 @@ import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; -import { BaseVisTypeOptions, Vis } from '../../visualizations/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore import tableVisTemplate from './table_vis.html'; @@ -99,7 +99,7 @@ export function getTableVisTypeDefinition( ]), }, responseHandler: tableVisResponseHandler, - hierarchicalData: (vis: Vis) => { + hierarchicalData: (vis) => { return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); }, }; diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts index 5e82796e66339..1781808660260 100644 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ b/src/plugins/vis_type_table/public/vis_controller.ts @@ -103,7 +103,9 @@ export function getTableVisualizationControllerClass( this.$scope = this.$rootScope.$new(); this.$scope.uiState = this.vis.getUiState(); updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); + this.el + .find('div') + .append(this.$compile(this.vis.type.visConfig?.template ?? '')(this.$scope)); this.$scope.$apply(); } else { updateScope(); diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.ts b/src/plugins/vis_type_tagcloud/public/to_ast.ts index a284bba307348..876784cc10140 100644 --- a/src/plugins/vis_type_tagcloud/public/to_ast.ts +++ b/src/plugins/vis_type_tagcloud/public/to_ast.ts @@ -38,7 +38,7 @@ export const toExpressionAst = (vis: Vis, params: BuildPipeli const esaggs = buildExpressionFunction('esaggs', { index: vis.data.indexPattern!.id!, metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.type.requiresPartialRows || false, + partialRows: false, aggConfigs: JSON.stringify(vis.data.aggs!.aggs), includeFormatHints: false, }); diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index b33215934c5df..0950938423134 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -104,6 +104,7 @@ export const metricsItems = schema.object({ }) ) ), + numberOfSignificantValueDigits: numberOptional, percentiles: schema.maybe( schema.arrayOf( schema.object({ diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js index f12c0c8f6f465..a34b74d106492 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile.js @@ -24,10 +24,11 @@ import { FieldSelect } from './field_select'; import { AggRow } from './agg_row'; import { createChangeHandler } from '../lib/create_change_handler'; import { createSelectHandler } from '../lib/create_select_handler'; +import { createNumberHandler } from '../lib/create_number_handler'; import { htmlIdGenerator, EuiSpacer, - EuiFlexGroup, + EuiFlexGrid, EuiFlexItem, EuiFormLabel, EuiFormRow, @@ -35,6 +36,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; import { Percentiles, newPercentile } from './percentile_ui'; +import { PercentileHdr } from './percentile_hdr'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; @@ -46,6 +48,8 @@ export function PercentileAgg(props) { const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); + const indexPattern = (series.override_index_pattern && series.series_index_pattern) || panel.index_pattern; @@ -66,7 +70,7 @@ export function PercentileAgg(props) { siblings={props.siblings} dragHandleProps={props.dragHandleProps} > - + - - - - - + + + } + > + + + + + + + ); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx new file mode 100644 index 0000000000000..c0ba7d3c6765b --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_hdr.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiFieldNumber, EuiFormRow, EuiIconTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface PercentileHdrProps { + value: number | undefined; + onChange: () => void; +} + +export const PercentileHdr = ({ value, onChange }: PercentileHdrProps) => ( + + {' '} + + } + type="questionInCircle" + /> + + } + > + + +); diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx index ef8876a19b1a6..444e11d0d563d 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/multi_value_row.tsx @@ -18,15 +18,8 @@ */ import React, { ChangeEvent } from 'react'; import { get } from 'lodash'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - htmlIdGenerator, - EuiFieldNumber, - EuiFlexGroup, - EuiFlexItem, - EuiFormLabel, - EuiSpacer, -} from '@elastic/eui'; + +import { EuiFieldNumber, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { AddDeleteButtons } from '../../add_delete_buttons'; @@ -50,8 +43,6 @@ export const MultiValueRow = ({ disableAdd, disableDelete, }: MultiValueRowProps) => { - const htmlId = htmlIdGenerator(); - const onFieldNumberChange = (event: ChangeEvent) => onChange({ ...model, @@ -59,17 +50,9 @@ export const MultiValueRow = ({ }); return ( -
- - - - - - - + + + onDelete(model)} disableDelete={disableDelete} disableAdd={disableAdd} + responsive={false} /> - -
+ ); }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx index d02a16ade2bba..f78df9b1ddef4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { htmlIdGenerator, - EuiFlexGroup, EuiFlexItem, EuiFormLabel, EuiFormRow, EuiSpacer, + EuiFlexGrid, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { AggSelect } from '../agg_select'; @@ -34,12 +34,16 @@ import { FieldSelect } from '../field_select'; import { createChangeHandler } from '../../lib/create_change_handler'; // @ts-ignore import { createSelectHandler } from '../../lib/create_select_handler'; +// @ts-ignore +import { createNumberHandler } from '../../lib/create_number_handler'; + import { AggRow } from '../agg_row'; import { PercentileRankValues } from './percentile_rank_values'; import { IFieldType, KBN_FIELD_TYPES } from '../../../../../../../plugins/data/public'; import { MetricsItemsSchema, PanelSchema, SeriesItemsSchema } from '../../../../../common/types'; import { DragHandleProps } from '../../../../types'; +import { PercentileHdr } from '../percentile_hdr'; const RESTRICT_FIELDS = [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; @@ -67,6 +71,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { const isTablePanel = panel.type === 'table'; const handleChange = createChangeHandler(props.onChange, model); const handleSelectChange = createSelectHandler(handleChange); + const handleNumberChange = createNumberHandler(handleChange); const handlePercentileRankValuesChange = (values: MetricsItemsSchema['values']) => { handleChange({ @@ -84,7 +89,7 @@ export const PercentileRankAgg = (props: PercentileRankAggProps) => { siblings={props.siblings} dragHandleProps={props.dragHandleProps} > - + { /> - - - {model.values && ( - - )} + + + + } + > + + + + + + + ); }; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx index b66d79d67f427..92ca4dfb706ac 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_rank/percentile_rank_values.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { last } from 'lodash'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { MultiValueRow } from './multi_value_row'; interface PercentileRankValuesProps { @@ -52,19 +52,20 @@ export const PercentileRankValues = (props: PercentileRankValuesProps) => { disableDeleteRow: boolean; disableAddRow: boolean; }) => ( - + + + ); return ( - + {showOnlyLastRow && renderRow({ rowModel: { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js index bd421248a3607..fb556a053df3f 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/percentile_ui.js @@ -19,6 +19,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import { collectionActions } from '../lib/collection_actions'; import { AddDeleteButtons } from '../add_delete_buttons'; @@ -27,17 +28,19 @@ import { htmlIdGenerator, EuiFlexGroup, EuiFlexItem, - EuiFormLabel, EuiComboBox, EuiFieldNumber, + EuiFormRow, + EuiFlexGrid, + EuiPanel, } from '@elastic/eui'; -import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; export const newPercentile = (opts) => { return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts); }; -class PercentilesUi extends Component { +export class Percentiles extends Component { handleTextChange(item, name) { return (e) => { const handleChange = collectionActions.handleChange.bind(null, this.props); @@ -50,22 +53,31 @@ class PercentilesUi extends Component { renderRow = (row, i, items) => { const defaults = { value: '', percentile: '', shade: '' }; const model = { ...defaults, ...row }; - const { intl, panel } = this.props; + const { panel } = this.props; + const flexItemStyle = { minWidth: 100 }; const percentileFieldNumber = ( - - + + + } + > + + ); @@ -77,99 +89,103 @@ class PercentilesUi extends Component { const handleDelete = collectionActions.handleDelete.bind(null, this.props, model); const modeOptions = [ { - label: intl.formatMessage({ - id: 'visTypeTimeseries.percentile.modeOptions.lineLabel', + label: i18n.translate('visTypeTimeseries.percentile.modeOptions.lineLabel', { defaultMessage: 'Line', }), value: 'line', }, { - label: intl.formatMessage({ - id: 'visTypeTimeseries.percentile.modeOptions.bandLabel', + label: i18n.translate('visTypeTimeseries.percentile.modeOptions.bandLabel', { defaultMessage: 'Band', }), value: 'band', }, ]; - const optionsStyle = {}; + const optionsStyle = { + ...flexItemStyle, + }; if (model.mode === 'line') { optionsStyle.display = 'none'; } - const labelStyle = { marginBottom: 0 }; + const htmlId = htmlIdGenerator(model.id); const selectedModeOption = modeOptions.find((option) => { return model.mode === option.value; }); return ( - - {percentileFieldNumber} - - - - - - - - - - - - - + + + + {percentileFieldNumber} + + + } + > + + + + + + } + > + + + + + + } + > + + + + + + + - - - - - - - - - - - - - - - - - - - + + + ); }; @@ -192,15 +208,13 @@ class PercentilesUi extends Component { } } -PercentilesUi.defaultProps = { +Percentiles.defaultProps = { name: 'percentile', }; -PercentilesUi.propTypes = { +Percentiles.propTypes = { name: PropTypes.string, model: PropTypes.object, panel: PropTypes.object, onChange: PropTypes.func, }; - -export const Percentiles = injectI18n(PercentilesUi); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js index c1d7aa9d40bd9..146e7a4bae15a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js @@ -25,6 +25,7 @@ export function getSupportedFieldsByMetricType(type) { case METRIC_TYPES.CARDINALITY: return Object.values(KBN_FIELD_TYPES).filter((t) => t !== KBN_FIELD_TYPES.HISTOGRAM); case METRIC_TYPES.VALUE_COUNT: + return Object.values(KBN_FIELD_TYPES); case METRIC_TYPES.AVERAGE: case METRIC_TYPES.SUM: return [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js index 3cd3fac191bf1..4aed5348c0c18 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js @@ -18,18 +18,23 @@ */ import { getSupportedFieldsByMetricType } from './get_supported_fields_by_metric_type'; +import { KBN_FIELD_TYPES } from '../../../../../../plugins/data/public'; describe('getSupportedFieldsByMetricType', () => { const shouldHaveHistogramAndNumbers = (type) => it(`should return numbers and histogram for ${type}`, () => { expect(getSupportedFieldsByMetricType(type)).toEqual(['number', 'histogram']); }); + const shouldSupportAllFieldTypes = (type) => + it(`should return all field types for ${type}`, () => { + expect(getSupportedFieldsByMetricType(type)).toEqual(Object.values(KBN_FIELD_TYPES)); + }); const shouldHaveOnlyNumbers = (type) => it(`should return only numbers for ${type}`, () => { expect(getSupportedFieldsByMetricType(type)).toEqual(['number']); }); - shouldHaveHistogramAndNumbers('value_count'); + shouldSupportAllFieldTypes('value_count'); shouldHaveHistogramAndNumbers('avg'); shouldHaveHistogramAndNumbers('sum'); diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index 1209a105af805..664751bbc0ec0 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -70,19 +70,20 @@ export const TimeSeries = ({ annotations, }) => { const chartRef = useRef(); - const updateCursor = (_, cursor) => { - if (chartRef.current) { - chartRef.current.dispatchExternalPointerEvent(cursor); - } - }; useEffect(() => { + const updateCursor = (_, cursor) => { + if (chartRef.current) { + chartRef.current.dispatchExternalPointerEvent(cursor); + } + }; + eventBus.on(ACTIVE_CURSOR, updateCursor); return () => { eventBus.off(ACTIVE_CURSOR, undefined, updateCursor); }; - }, []); // eslint-disable-line + }, []); const tooltipFormatter = decorateFormatter(xAxisFormatter); const uiSettings = getUISettings(); @@ -139,6 +140,7 @@ export const TimeSeries = ({ type: tooltipMode === 'show_focused' ? TooltipType.Follow : TooltipType.VerticalCursor, headerFormatter: tooltipFormatter, }} + externalPointerEvents={{ tooltip: { visible: false } }} /> {annotations.map(({ id, data, icon, color }) => { diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js index 1eace13c2e336..232efc7514a5a 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js @@ -43,7 +43,6 @@ export async function getSeriesData(req, panel) { (acc, items) => acc.concat(items), [] ); - const data = await searchStrategy.search(req, searches); const handleResponseBodyFn = handleResponseBody(panel); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index f033a43806312..dc2936072165e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -64,6 +64,16 @@ function extendStatsBucket(bucket, metrics) { return body; } +function getPercentileHdrParam(bucket) { + return bucket.numberOfSignificantValueDigits + ? { + hdr: { + number_of_significant_value_digits: bucket.numberOfSignificantValueDigits, + }, + } + : undefined; +} + export const bucketTransform = { count: () => { return { @@ -139,13 +149,14 @@ export const bucketTransform = { bucket.percentiles.filter((p) => p.percentile).map((p) => p.percentile) ); } - const agg = { + + return { percentiles: { field: bucket.field, percents, + ...getPercentileHdrParam(bucket), }, }; - return agg; }, percentile_rank: (bucket) => { @@ -155,6 +166,7 @@ export const bucketTransform = { percentile_ranks: { field: bucket.field, values: (bucket.values || []).map((value) => (isEmpty(value) ? 0 : value)), + ...getPercentileHdrParam(bucket), }, }; }, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js index a11324f73e611..c163605af7ac5 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/percentile_rank.js @@ -32,7 +32,7 @@ export function percentileRank(resp, panel, series, meta) { } getSplits(resp, panel, series, meta).forEach((split) => { - (metric.values || []).forEach((percentileRank) => { + (metric.values || []).forEach((percentileRank, index) => { const data = split.timeseries.buckets.map((bucket) => [ bucket.key, getAggValue(bucket, { @@ -43,7 +43,7 @@ export function percentileRank(resp, panel, series, meta) { results.push({ data, - id: `${split.id}:${percentileRank}`, + id: `${split.id}:${percentileRank}:${index}`, label: `${split.label} (${percentileRank || 0})`, color: split.color, ...getDefaultDecoration(series), diff --git a/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx b/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx new file mode 100644 index 0000000000000..4f8bc50bb1b3b --- /dev/null +++ b/src/plugins/vis_type_vega/public/components/experimental_map_vis_info.tsx @@ -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 { parse } from 'hjson'; +import React from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Vis } from '../../../visualizations/public'; + +function ExperimentalMapLayerInfo() { + const title = ( + + GitHub + + ), + }} + /> + ); + + return ( + + ); +} + +export const getInfoMessage = (vis: Vis) => { + if (vis.params.spec) { + try { + const spec = parse(vis.params.spec, { legacyRoot: false, keepWsc: true }); + + if (spec.config?.kibana?.type === 'map') { + return ; + } + } catch (e) { + // spec is invalid + } + } + + return null; +}; diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 46fd2fbc5587e..0496f765e5e99 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; @@ -29,13 +30,18 @@ import { getDefaultSpec } from './default_spec'; import { createInspectorAdapters } from './vega_inspector'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; -export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { +import { getInfoMessage } from './components/experimental_map_vis_info'; + +export const createVegaTypeDefinition = ( + dependencies: VegaVisualizationDependencies +): BaseVisTypeOptions => { const requestHandler = createVegaRequestHandler(dependencies); const visualization = createVegaVisualization(dependencies); return { name: 'vega', title: 'Vega', + getInfoMessage, description: i18n.translate('visTypeVega.type.vegaDescription', { defaultMessage: 'Create custom visualizations using Vega and Vega-Lite', description: 'Vega and Vega-Lite are product names and should not be translated', diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx index 524792d1460fe..0cc737f19e5c6 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.test.tsx @@ -134,34 +134,6 @@ describe('MetricsAxisOptions component', () => { const updatedSeries = [{ ...chart, data: { id: agg.id, label: agg.makeLabel() } }]; expect(setValue).toHaveBeenCalledWith(SERIES_PARAMS, updatedSeries); }); - - it('should update visType when one seriesParam', () => { - const comp = mount(); - expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); - - comp.setProps({ - stateParams: { - ...defaultProps.stateParams, - seriesParams: [{ ...chart, type: ChartTypes.LINE }], - }, - }); - - expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.LINE }); - }); - - it('should set histogram visType when multiple seriesParam', () => { - const comp = mount(); - expect(defaultProps.vis.type.type).toBe(ChartTypes.AREA); - - comp.setProps({ - stateParams: { - ...defaultProps.stateParams, - seriesParams: [chart, { ...chart, type: ChartTypes.LINE }], - }, - }); - - expect(defaultProps.vis.setState).toHaveBeenLastCalledWith({ type: ChartTypes.HISTOGRAM }); - }); }); describe('updateAxisTitle', () => { diff --git a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx index d885f8fb0b12f..18687404b9114 100644 --- a/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx +++ b/src/plugins/vis_type_vislib/public/components/options/metrics_axes/index.tsx @@ -18,7 +18,7 @@ */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { cloneDeep, uniq, get } from 'lodash'; +import { cloneDeep, get } from 'lodash'; import { EuiSpacer } from '@elastic/eui'; import { IAggConfig } from 'src/plugins/data/public'; @@ -293,15 +293,6 @@ function MetricsAxisOptions(props: ValidationVisOptionsProps) updateAxisTitle(updatedSeries); }, [metrics, firstValueAxesId, setValue, stateParams.seriesParams, updateAxisTitle]); - const visType = useMemo(() => { - const types = uniq(stateParams.seriesParams.map(({ type }) => type)); - return types.length === 1 ? types[0] : 'histogram'; - }, [stateParams.seriesParams]); - - useEffect(() => { - vis.setState({ ...vis.serialize(), type: visType }); - }, [vis, visType]); - return isTabSelected ? ( <> ) { const { stateParams, setValue, vis } = props; + const currentChartTypes = useMemo(() => uniq(stateParams.seriesParams.map(({ type }) => type)), [ + stateParams.seriesParams, + ]); + return ( <> @@ -68,7 +73,7 @@ function PointSeriesOptions(props: ValidationVisOptionsProps) /> )} - {vis.type.name === ChartTypes.HISTOGRAM && ( + {currentChartTypes.includes(ChartTypes.HISTOGRAM) && ( { const { vis } = this.props; const Visualization = vis.type.visualization; + if (!Visualization) { + throw new Error( + 'Tried to use VisualizationChart component with a vis without visualization property.' + ); + } + this.visualization = new Visualization(this.chartDiv.current, vis); // We know that containerDiv.current will never be null, since we will always diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 75e53e8e92dbe..87f78f5639ff0 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -18,7 +18,7 @@ */ import { i18n } from '@kbn/i18n'; -import { SavedObjectMetaData } from 'src/plugins/saved_objects/public'; +import { SavedObjectMetaData, OnSaveProps } from 'src/plugins/saved_objects/public'; import { first } from 'rxjs/operators'; import { SavedObjectAttributes } from '../../../../core/public'; import { @@ -51,6 +51,7 @@ import { StartServicesGetter } from '../../../kibana_utils/public'; import { VisualizationsStartDeps } from '../plugin'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; import { AttributeService } from '../../../dashboard/public'; +import { checkForDuplicateTitle } from '../../../saved_objects/public'; interface VisualizationAttributes extends SavedObjectAttributes { visState: string; @@ -58,7 +59,7 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick + Pick >; } @@ -129,7 +130,10 @@ export class VisualizeEmbeddableFactory VisualizeSavedObjectAttributes, VisualizeByValueInput, VisualizeByReferenceInput - >(this.type, { customSaveMethod: this.onSave }); + >(this.type, { + saveMethod: this.saveMethod.bind(this), + checkForDuplicateTitle: this.checkTitle.bind(this), + }); } return this.attributeService!; } @@ -183,7 +187,7 @@ export class VisualizeEmbeddableFactory } } - private async onSave( + private async saveMethod( type: string, attributes: VisualizeSavedObjectAttributes ): Promise<{ id: string }> { @@ -225,4 +229,24 @@ export class VisualizeEmbeddableFactory throw error; } } + + public async checkTitle(props: OnSaveProps): Promise { + const savedObjectsClient = await this.deps.start().core.savedObjects.client; + const overlays = await this.deps.start().core.overlays; + return checkForDuplicateTitle( + { + title: props.newTitle, + copyOnSave: false, + lastSavedTitle: '', + getEsType: () => this.type, + getDisplayName: this.getDisplayName || (() => this.type), + }, + props.isTitleDuplicateConfirmed, + props.onTitleDuplicate, + { + savedObjectsClient, + overlays, + } + ); + } } diff --git a/src/plugins/visualizations/public/expressions/vis.ts b/src/plugins/visualizations/public/expressions/vis.ts index 5a99dceda20bd..87f77e589c9ca 100644 --- a/src/plugins/visualizations/public/expressions/vis.ts +++ b/src/plugins/visualizations/public/expressions/vis.ts @@ -36,7 +36,7 @@ import { VisType } from '../vis_types'; export interface ExprVisState { title?: string; - type: VisType | string; + type: VisType | string; params?: VisParams; } @@ -52,7 +52,7 @@ export interface ExprVisAPI { export class ExprVis extends EventEmitter { public title: string = ''; - public type: VisType; + public type: VisType; public params: VisParams = {}; public sessionState: Record = {}; public API: ExprVisAPI; @@ -92,7 +92,7 @@ export class ExprVis extends EventEmitter { }; } - private getType(type: string | VisType) { + private getType(type: string | VisType) { if (_.isString(type)) { const newType = getTypes().get(type); if (!newType) { diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 79e1c1cca2155..9f6a4d5553292 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -86,7 +86,10 @@ const vislibCharts: string[] = [ 'line', ]; -export const getSchemas = (vis: Vis, { timeRange, timefilter }: BuildPipelineParams): Schemas => { +export const getSchemas = ( + vis: Vis, + { timeRange, timefilter }: BuildPipelineParams +): Schemas => { const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { if (isDateHistogramBucketAggConfig(agg)) { agg.params.timeRange = timeRange; @@ -155,7 +158,8 @@ export const getSchemas = (vis: Vis, { timeRange, timefilter }: BuildPipelinePar } } if (schemaName === 'split') { - schemaName = `split_${vis.params.row ? 'row' : 'column'}`; + // TODO: We should check if there's a better way then casting to `any` here + schemaName = `split_${(vis.params as any).row ? 'row' : 'column'}`; skipMetrics = responseAggs.length - metrics.length > 1; } if (!schemas[schemaName]) { @@ -410,7 +414,7 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { pipeline += `esaggs ${prepareString('index', indexPattern!.id)} metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} + partialRows=${vis.params.showPartialRows || false} ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; } @@ -433,7 +437,7 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { pipeline += `visualization type='${vis.type.name}' ${prepareJson('visConfig', visConfig)} metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `; + partialRows=${vis.params.showPartialRows || false} `; if (indexPattern) { pipeline += `${prepareString('index', indexPattern.id)} `; if (vis.data.aggs) { diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 646acc49a6a83..90e4936a58b45 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -72,6 +72,7 @@ const createInstance = async () => { embeddable: embeddablePluginMock.createStartContract(), dashboard: dashboardPluginMock.createStartContract(), getAttributeService: jest.fn(), + savedObjectsClient: coreMock.createStart().savedObjects.client, }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index 0ba80887b513f..37a9972983421 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -25,6 +25,7 @@ import { CoreStart, Plugin, ApplicationStart, + SavedObjectsClientContract, } from '../../../core/public'; import { TypesService, TypesSetup, TypesStart } from './vis_types'; import { @@ -112,6 +113,7 @@ export interface VisualizationsStartDeps { application: ApplicationStart; dashboard: DashboardStart; getAttributeService: DashboardStart['getAttributeService']; + savedObjectsClient: SavedObjectsClientContract; } /** diff --git a/src/plugins/visualizations/public/vis.test.ts b/src/plugins/visualizations/public/vis.test.ts index c271888b7c7a4..e1b188f2e460b 100644 --- a/src/plugins/visualizations/public/vis.test.ts +++ b/src/plugins/visualizations/public/vis.test.ts @@ -121,7 +121,7 @@ describe('Vis Class', function () { }); it('should return true for hierarchical vis (like pie)', function () { - vis.type.hierarchicalData = true; + (vis.type as any).hierarchicalData = true; expect(vis.isHierarchical()).toBe(true); }); }); diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index c6773e5a1bee3..5c3233a8de896 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -84,7 +84,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: type PartialVisState = Assign }>; export class Vis { - public readonly type: VisType; + public readonly type: VisType; public readonly id?: string; public title: string = ''; public description: string = ''; @@ -97,14 +97,14 @@ export class Vis { public readonly uiState: PersistedState; constructor(visType: string, visState: SerializedVis = {} as any) { - this.type = this.getType(visType); + this.type = this.getType(visType); this.params = this.getParams(visState.params); this.uiState = new PersistedState(visState.uiState); this.id = visState.id; } - private getType(visType: string) { - const type = getTypes().get(visType); + private getType(visType: string) { + const type = getTypes().get(visType); if (!type) { const errorMessage = i18n.translate('visualizations.visualizationTypeInvalidMessage', { defaultMessage: 'Invalid visualization type "{visType}"', @@ -118,7 +118,7 @@ export class Vis { } private getParams(params: VisParams) { - return defaults({}, cloneDeep(params || {}), cloneDeep(this.type.visConfig.defaults || {})); + return defaults({}, cloneDeep(params ?? {}), cloneDeep(this.type.visConfig?.defaults ?? {})); } async setState(state: PartialVisState) { @@ -202,10 +202,6 @@ export class Vis { }; } - toExpressionAst() { - return this.type.toExpressionAst(this.params); - } - // deprecated isHierarchical() { if (isFunction(this.type.hierarchicalData)) { diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index de1afc254e0d3..f2933de723a39 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -17,118 +17,113 @@ * under the License. */ -import _ from 'lodash'; -import { ReactElement } from 'react'; -import { VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; -import { TriggerContextMapping } from '../../../ui_actions/public'; -import { Adapters } from '../../../inspector/public'; -import { Vis } from '../vis'; +import { defaultsDeep } from 'lodash'; +import { ISchemas } from 'src/plugins/vis_default_editor/public'; +import { VisParams } from '../types'; +import { VisType, VisTypeOptions } from './types'; -interface CommonBaseVisTypeOptions { - name: string; - title: string; - description?: string; - getSupportedTriggers?: () => Array; - icon?: string; - image?: string; - stage?: 'experimental' | 'beta' | 'production'; - options?: Record; - visConfig?: Record; - editor?: any; - editorConfig?: Record; - hidden?: boolean; - requestHandler?: string | unknown; - responseHandler?: string | unknown; - hierarchicalData?: boolean | unknown; - setup?: unknown; - useCustomNoDataScreen?: boolean; - inspectorAdapters?: Adapters | (() => Adapters); - isDeprecated?: boolean; - getDeprecationMessage?: (vis: Vis) => ReactElement<{}>; +interface CommonBaseVisTypeOptions + extends Pick< + VisType, + | 'description' + | 'editor' + | 'getInfoMessage' + | 'getSupportedTriggers' + | 'hierarchicalData' + | 'icon' + | 'image' + | 'inspectorAdapters' + | 'name' + | 'requestHandler' + | 'responseHandler' + | 'setup' + | 'title' + >, + Pick< + Partial>, + 'editorConfig' | 'hidden' | 'stage' | 'useCustomNoDataScreen' | 'visConfig' + > { + options?: Partial['options']>; } -interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { - toExpressionAst: VisToExpressionAst; +interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { + toExpressionAst: VisType['toExpressionAst']; visualization?: undefined; } -interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { +interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { toExpressionAst?: undefined; - visualization: VisualizationControllerConstructor | undefined; + visualization: VisType['visualization']; } export type BaseVisTypeOptions = | ExpressionBaseVisTypeOptions - | VisualizationBaseVisTypeOptions; + | VisualizationBaseVisTypeOptions; -export class BaseVisType { - name: string; - title: string; - description: string; - getSupportedTriggers?: () => Array; - icon?: string; - image?: string; - stage: 'experimental' | 'beta' | 'production'; - isExperimental: boolean; - options: Record; - visualization: VisualizationControllerConstructor | undefined; - visConfig: Record; - editor: any; - editorConfig: Record; - hidden: boolean; - requiresSearch: boolean; - requestHandler: string | unknown; - responseHandler: string | unknown; - hierarchicalData: boolean | unknown; - setup?: unknown; - useCustomNoDataScreen: boolean; - inspectorAdapters?: Adapters | (() => Adapters); - toExpressionAst?: VisToExpressionAst; - getDeprecationMessage?: (vis: Vis) => ReactElement<{}>; +const defaultOptions: VisTypeOptions = { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, // we should get rid of this i guess ? +}; + +export class BaseVisType implements VisType { + public readonly name; + public readonly title; + public readonly description; + public readonly getSupportedTriggers; + public readonly icon; + public readonly image; + public readonly stage; + public readonly options; + public readonly visualization; + public readonly visConfig; + public readonly editor; + public readonly editorConfig; + public hidden; + public readonly requestHandler; + public readonly responseHandler; + public readonly hierarchicalData; + public readonly setup; + public readonly useCustomNoDataScreen; + public readonly inspectorAdapters; + public readonly toExpressionAst; + public readonly getInfoMessage; constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { throw new Error('vis_type must define its icon or image'); } - const defaultOptions = { - // controls the visualize editor - showTimePicker: true, - showQueryBar: true, - showFilterBar: true, - showIndexSelection: true, - hierarchicalData: false, // we should get rid of this i guess ? - }; - this.name = opts.name; - this.description = opts.description || ''; + this.description = opts.description ?? ''; this.getSupportedTriggers = opts.getSupportedTriggers; this.title = opts.title; this.icon = opts.icon; this.image = opts.image; this.visualization = opts.visualization; - this.visConfig = _.defaultsDeep({}, opts.visConfig, { defaults: {} }); + this.visConfig = defaultsDeep({}, opts.visConfig, { defaults: {} }); this.editor = opts.editor; - this.editorConfig = _.defaultsDeep({}, opts.editorConfig, { collections: {} }); - this.options = _.defaultsDeep({}, opts.options, defaultOptions); - this.stage = opts.stage || 'production'; - this.isExperimental = opts.stage === 'experimental'; - this.hidden = opts.hidden || false; - this.requestHandler = opts.requestHandler || 'courier'; - this.responseHandler = opts.responseHandler || 'none'; + this.editorConfig = defaultsDeep({}, opts.editorConfig, { collections: {} }); + this.options = defaultsDeep({}, opts.options, defaultOptions); + this.stage = opts.stage ?? 'production'; + this.hidden = opts.hidden ?? false; + this.requestHandler = opts.requestHandler ?? 'courier'; + this.responseHandler = opts.responseHandler ?? 'none'; this.setup = opts.setup; - this.requiresSearch = this.requestHandler !== 'none'; - this.hierarchicalData = opts.hierarchicalData || false; - this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false; + this.hierarchicalData = opts.hierarchicalData ?? false; + this.useCustomNoDataScreen = opts.useCustomNoDataScreen ?? false; this.inspectorAdapters = opts.inspectorAdapters; this.toExpressionAst = opts.toExpressionAst; - this.getDeprecationMessage = opts.getDeprecationMessage; + this.getInfoMessage = opts.getInfoMessage; } - public get schemas() { - if (this.editorConfig && this.editorConfig.schemas) { - return this.editorConfig.schemas; - } - return []; + public get schemas(): ISchemas { + return this.editorConfig?.schemas ?? []; + } + + public get requiresSearch(): boolean { + return this.requestHandler !== 'none'; } } diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 8f38e33569162..22561decabea4 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,5 +18,6 @@ */ export * from './types_service'; +export { VisType } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/react_vis_type.ts b/src/plugins/visualizations/public/vis_types/react_vis_type.ts index 047d36d804111..f6bd51df26695 100644 --- a/src/plugins/visualizations/public/vis_types/react_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/react_vis_type.ts @@ -19,15 +19,21 @@ import { BaseVisType, BaseVisTypeOptions } from './base_vis_type'; import { ReactVisController } from './react_vis_controller'; +import { VisType } from './types'; -export type ReactVisTypeOptions = Omit; +export type ReactVisTypeOptions = Omit< + BaseVisTypeOptions, + 'visualization' | 'toExpressionAst' +>; /** * This class should only be used for visualizations not using the `toExpressionAst` with a custom renderer. * If you implement a custom renderer you should just mount a react component inside this. */ -export class ReactVisType extends BaseVisType { - constructor(opts: ReactVisTypeOptions) { +export class ReactVisType + extends BaseVisType + implements VisType { + constructor(opts: ReactVisTypeOptions) { super({ ...opts, visualization: ReactVisController, diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts new file mode 100644 index 0000000000000..0cf345bf07be6 --- /dev/null +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IconType } from '@elastic/eui'; +import React from 'react'; +import { Adapters } from 'src/plugins/inspector'; +import { ISchemas } from 'src/plugins/vis_default_editor/public'; +import { TriggerContextMapping } from '../../../ui_actions/public'; +import { Vis, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; + +export interface VisTypeOptions { + showTimePicker: boolean; + showQueryBar: boolean; + showFilterBar: boolean; + showIndexSelection: boolean; + hierarchicalData: boolean; +} + +/** + * A visualization type representing one specific type of "classical" + * visualizations (i.e. not Lens visualizations). + */ +export interface VisType { + readonly name: string; + readonly title: string; + readonly description?: string; + readonly getSupportedTriggers?: () => Array; + readonly isAccessible?: boolean; + readonly requestHandler?: string | unknown; + readonly responseHandler?: string | unknown; + readonly icon?: IconType; + readonly image?: string; + readonly stage: 'experimental' | 'beta' | 'production'; + readonly requiresSearch: boolean; + readonly useCustomNoDataScreen: boolean; + readonly hierarchicalData?: boolean | ((vis: { params: TVisParams }) => boolean); + readonly inspectorAdapters?: Adapters | (() => Adapters); + /** + * When specified this visualization is deprecated. This function + * should return a ReactElement that will render a deprecation warning. + * It will be shown in the editor when editing/creating visualizations + * of this type. + */ + readonly getInfoMessage?: (vis: Vis) => React.ReactNode; + + readonly toExpressionAst?: VisToExpressionAst; + readonly visualization?: VisualizationControllerConstructor; + + readonly setup?: (vis: Vis) => Promise>; + hidden: boolean; + + readonly schemas: ISchemas; + + readonly options: VisTypeOptions; + + // TODO: The following types still need to be refined properly. + + /** + * The editor that should be used to edit visualizations of this type. + */ + readonly editor?: any; + readonly editorConfig: Record; + readonly visConfig: Record; +} diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 1afbd6901a195..5d619064c240e 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -17,33 +17,10 @@ * under the License. */ -import { IconType } from '@elastic/eui'; import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; import { BaseVisType, BaseVisTypeOptions } from './base_vis_type'; import { ReactVisType, ReactVisTypeOptions } from './react_vis_type'; -import { TriggerContextMapping } from '../../../ui_actions/public'; - -export interface VisType { - name: string; - title: string; - description?: string; - getSupportedTriggers?: () => Array; - visualization: any; - isAccessible?: boolean; - requestHandler: string | unknown; - responseHandler: string | unknown; - icon?: IconType; - image?: string; - stage: 'experimental' | 'beta' | 'production'; - requiresSearch: boolean; - hidden: boolean; - - // Since we haven't typed everything here yet, we basically "any" the rest - // of that interface. This should be removed as soon as this type definition - // has been completed. But that way we at least have typing for a couple of - // properties on that type. - [key: string]: any; -} +import { VisType } from './types'; /** * Vis Types Service @@ -51,21 +28,21 @@ export interface VisType { * @internal */ export class TypesService { - private types: Record = {}; + private types: Record> = {}; private unregisteredHiddenTypes: string[] = []; - public setup() { - const registerVisualization = (registerFn: () => VisType) => { - const visDefinition = registerFn(); - if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { - visDefinition.hidden = true; - } + private registerVisualization(visDefinition: VisType) { + if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { + visDefinition.hidden = true; + } - if (this.types[visDefinition.name]) { - throw new Error('type already exists!'); - } - this.types[visDefinition.name] = visDefinition; - }; + if (this.types[visDefinition.name]) { + throw new Error('type already exists!'); + } + this.types[visDefinition.name] = visDefinition; + } + + public setup() { return { /** * registers a visualization type @@ -73,15 +50,15 @@ export class TypesService { */ createBaseVisualization: (config: BaseVisTypeOptions): void => { const vis = new BaseVisType(config); - registerVisualization(() => vis); + this.registerVisualization(vis); }, /** * registers a visualization which uses react for rendering * @param config - visualization type definition */ - createReactVisualization: (config: ReactVisTypeOptions): void => { + createReactVisualization: (config: ReactVisTypeOptions): void => { const vis = new ReactVisType(config); - registerVisualization(() => vis); + this.registerVisualization(vis); }, /** * registers a visualization alias @@ -93,7 +70,7 @@ export class TypesService { * allows to hide specific visualization types from create visualization dialog * @param {string[]} typeNames - list of type ids to hide */ - hideTypes: (typeNames: string[]) => { + hideTypes: (typeNames: string[]): void => { typeNames.forEach((name: string) => { if (this.types[name]) { this.types[name].hidden = true; @@ -111,13 +88,13 @@ export class TypesService { * returns specific visualization or undefined if not found * @param {string} visualization - id of visualization to return */ - get: (visualization: string) => { + get: (visualization: string): VisType => { return this.types[visualization]; }, /** * returns all registered visualization types */ - all: () => { + all: (): VisType[] => { return [...Object.values(this.types)]; }, /** diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index c2a2b27457f8d..2d55059efb5bb 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -238,12 +238,43 @@ exports[`NewVisModal filter for visualization types should render as expected 1` aria-live="polite" class="euiScreenReaderOnly" > - 2 types found + 3 types found
    +
  • + +
@@ -605,11 +659,11 @@ exports[`NewVisModal filter for visualization types should render as expected 1` id="visualizations.newVisWizard.resultsFound" values={ Object { - "resultCount": 2, + "resultCount": 3, } } > - 2 types found + 3 types found @@ -621,6 +675,75 @@ exports[`NewVisModal filter for visualization types should render as expected 1` className="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + + Vis alias with promotion + + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + > + + +
  • @@ -867,7 +990,21 @@ exports[`NewVisModal filter for visualization types should render as expected 1`

    +

    + + promotion description + +

    + + + + +
    @@ -1129,6 +1326,37 @@ exports[`NewVisModal should render as expected 1`] = ` class="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + +
  • @@ -1454,6 +1705,75 @@ exports[`NewVisModal should render as expected 1`] = ` className="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + + Vis alias with promotion + + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + > + + +
  • @@ -1700,7 +2020,21 @@ exports[`NewVisModal should render as expected 1`] = `

    +

    + + promotion description + +

    + + + + +
    diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index f48febfef5b43..51bcfed201687 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -51,13 +51,24 @@ describe('NewVisModal', () => { aliasApp: 'otherApp', aliasPath: '#/aliasUrl', }, + { + name: 'visAliasWithPromotion', + title: 'Vis alias with promotion', + stage: 'production', + aliasApp: 'anotherApp', + aliasPath: '#/anotherUrl', + promotion: { + description: 'promotion description', + buttonText: 'another app', + }, + }, ]; const visTypes: TypesStart = { - get: (id: string) => { - return _visTypes.find((vis) => vis.name === id) as VisType; + get(id: string): VisType { + return (_visTypes.find((vis) => vis.name === id) as unknown) as VisType; }, all: () => { - return _visTypes as VisType[]; + return (_visTypes as unknown) as VisType[]; }, getAliases: () => [], }; @@ -107,6 +118,30 @@ describe('NewVisModal', () => { expect(wrapper.find('[data-test-subj="visType-vis"]').exists()).toBe(true); }); + it('should sort promoted visualizations first', () => { + const wrapper = mountWithIntl( + null} + visTypesRegistry={visTypes} + addBasePath={addBasePath} + uiSettings={uiSettings} + application={{} as ApplicationStart} + savedObjects={{} as SavedObjectsStart} + /> + ); + expect( + wrapper + .find('button[data-test-subj^="visType-"]') + .map((button) => button.prop('data-test-subj')) + ).toEqual([ + 'visType-visAliasWithPromotion', + 'visType-vis', + 'visType-visWithAliasUrl', + 'visType-visWithSearch', + ]); + }); + describe('open editor', () => { it('should open the editor for visualizations without search', () => { const wrapper = mountWithIntl( diff --git a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx index fa15a6c9ba02b..a5b6e8039ba6d 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx @@ -31,7 +31,6 @@ describe('NewVisHelp', () => { aliasApp: 'myApp', aliasPath: '/my/fancy/new/thing', description: 'Some desc', - highlighted: false, icon: 'whatever', name: 'whatever', promotion: { diff --git a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx index fc48438904589..5b226a889408f 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx @@ -20,11 +20,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { EuiText, EuiButton } from '@elastic/eui'; -import { VisTypeAliasListEntry } from './type_selection'; import { VisTypeAlias } from '../../vis_types'; interface Props { - promotedTypes: VisTypeAliasListEntry[]; + promotedTypes: VisTypeAlias[]; onPromotionClicked: (visType: VisTypeAlias) => void; } diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index f507635093f7f..8c086ed132ae4 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -42,11 +42,8 @@ import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; import { VisType, TypesStart } from '../../vis_types'; -export interface VisTypeListEntry extends VisType { - highlighted: boolean; -} - -export interface VisTypeAliasListEntry extends VisTypeAlias { +interface VisTypeListEntry { + type: VisType | VisTypeAlias; highlighted: boolean; } @@ -69,6 +66,10 @@ interface TypeSelectionState { query: string; } +function isVisTypeAlias(type: VisType | VisTypeAlias): type is VisTypeAlias { + return 'aliasPath' in type; +} + class TypeSelection extends React.Component { public state: TypeSelectionState = { highlightedType: null, @@ -155,7 +156,9 @@ class TypeSelection extends React.Component t.promotion)} + promotedTypes={visTypes + .map((t) => t.type) + .filter((t): t is VisTypeAlias => isVisTypeAlias(t) && Boolean(t.promotion))} onPromotionClicked={this.props.onVisTypeSelected} /> @@ -167,10 +170,7 @@ class TypeSelection extends React.Component { + private filteredVisTypes(visTypes: TypesStart, query: string): VisTypeListEntry[] { const types = visTypes.all().filter((type) => { // Filter out all lab visualizations if lab mode is not enabled if (!this.props.showExperimental && type.stage === 'experimental') { @@ -187,9 +187,9 @@ class TypeSelection extends React.Component; + let entries: VisTypeListEntry[]; if (!query) { - entries = allTypes.map((type) => ({ ...type, highlighted: false })); + entries = allTypes.map((type) => ({ type, highlighted: false })); } else { const q = query.toLowerCase(); entries = allTypes.map((type) => { @@ -197,17 +197,21 @@ class TypeSelection extends React.Component { + private renderVisType = (visType: VisTypeListEntry) => { let stage = {}; let highlightMsg; - if (!('aliasPath' in visType) && visType.stage === 'experimental') { + if (!isVisTypeAlias(visType.type) && visType.type.stage === 'experimental') { stage = { betaBadgeLabel: i18n.translate('visualizations.newVisWizard.experimentalTitle', { defaultMessage: 'Experimental', @@ -221,7 +225,7 @@ class TypeSelection extends React.Component this.props.onVisTypeSelected(visType); + const onClick = () => this.props.onVisTypeSelected(visType.type); const highlightedType: HighlightedType = { - title: visType.title, - name: visType.name, - description: visType.description, + title: visType.type.title, + name: visType.type.name, + description: visType.type.description, highlightMsg, }; return ( {visType.title}} + key={visType.type.name} + label={{visType.type.title}} onClick={onClick} onFocus={() => this.setHighlightType(highlightedType)} onMouseEnter={() => this.setHighlightType(highlightedType)} onMouseLeave={() => this.setHighlightType(null)} onBlur={() => this.setHighlightType(null)} className="visNewVisDialog__type" - data-test-subj={`visType-${visType.name}`} - data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'} + data-test-subj={`visType-${visType.type.name}`} + data-vis-stage={!isVisTypeAlias(visType.type) ? visType.type.stage : 'alias'} disabled={isDisabled} - aria-describedby={`visTypeDescription-${visType.name}`} + aria-describedby={`visTypeDescription-${visType.type.name}`} {...stage} > ); diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index ad7618a8640ba..5261b2cac7dcf 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -45,13 +45,13 @@ export const visualizationSavedObjectType: SavedObjectsType = { properties: { description: { type: 'text' }, kibanaSavedObjectMeta: { - properties: { searchSourceJSON: { type: 'text', index: false, doc_values: false } }, + properties: { searchSourceJSON: { type: 'text', index: false } }, }, savedSearchRefName: { type: 'keyword', index: false, doc_values: false }, title: { type: 'text' }, - uiStateJSON: { type: 'text', index: false, doc_values: false }, + uiStateJSON: { type: 'text', index: false }, version: { type: 'integer' }, - visState: { type: 'text', index: false, doc_values: false }, + visState: { type: 'text', index: false }, }, }, migrations: visualizationSavedObjectTypeMigrations, diff --git a/src/plugins/visualize/public/actions/visualize_field_action.ts b/src/plugins/visualize/public/actions/visualize_field_action.ts index 6671d2c981910..e570ed5e49e6a 100644 --- a/src/plugins/visualize/public/actions/visualize_field_action.ts +++ b/src/plugins/visualize/public/actions/visualize_field_action.ts @@ -34,6 +34,7 @@ import { AGGS_TERMS_SIZE_SETTING } from '../../common/constants'; export const visualizeFieldAction = createAction({ type: ACTION_VISUALIZE_FIELD, + id: ACTION_VISUALIZE_FIELD, getDisplayName: () => i18n.translate('visualize.discover.visualizeFieldLabel', { defaultMessage: 'Visualize field', diff --git a/src/plugins/visualize/public/application/app.tsx b/src/plugins/visualize/public/application/app.tsx index 8dd6b2ace8413..bf11cde3115aa 100644 --- a/src/plugins/visualize/public/application/app.tsx +++ b/src/plugins/visualize/public/application/app.tsx @@ -21,6 +21,7 @@ import './app.scss'; import React, { useEffect } from 'react'; import { Route, Switch, useLocation } from 'react-router-dom'; +import { AppMountParameters } from 'kibana/public'; import { syncQueryStateWithUrl } from '../../../data/public'; import { useKibana } from '../../../kibana_react/public'; import { VisualizeServices } from './types'; @@ -32,7 +33,11 @@ import { } from './components'; import { VisualizeConstants } from './visualize_constants'; -export const VisualizeApp = () => { +export interface VisualizeAppProps { + onAppLeave: AppMountParameters['onAppLeave']; +} + +export const VisualizeApp = ({ onAppLeave }: VisualizeAppProps) => { const { services: { data: { query }, @@ -54,10 +59,10 @@ export const VisualizeApp = () => { return ( - + - + { +export const VisualizeByValueEditor = ({ onAppLeave }: VisualizeAppProps) => { const [originatingApp, setOriginatingApp] = useState(); const { services } = useKibana(); const [eventEmitter] = useState(new EventEmitter()); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(true); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [embeddableId, setEmbeddableId] = useState(); const [valueInput, setValueInput] = useState(); @@ -100,6 +101,7 @@ export const VisualizeByValueEditor = () => { setHasUnsavedChanges={setHasUnsavedChanges} visEditorRef={visEditorRef} embeddableId={embeddableId} + onAppLeave={onAppLeave} /> ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_editor.tsx b/src/plugins/visualize/public/application/components/visualize_editor.tsx index 6a0bd26a16faa..7c0fa065c3a71 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor.tsx @@ -32,8 +32,9 @@ import { } from '../utils'; import { VisualizeServices } from '../types'; import { VisualizeEditorCommon } from './visualize_editor_common'; +import { VisualizeAppProps } from '../app'; -export const VisualizeEditor = () => { +export const VisualizeEditor = ({ onAppLeave }: VisualizeAppProps) => { const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); const [originatingApp, setOriginatingApp] = useState(); const { services } = useKibana(); @@ -91,6 +92,7 @@ export const VisualizeEditor = () => { visualizationIdFromUrl={visualizationIdFromUrl} setHasUnsavedChanges={setHasUnsavedChanges} visEditorRef={visEditorRef} + onAppLeave={onAppLeave} /> ); }; diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index 4b7b4dae02d0a..947385b05d458 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -20,6 +20,7 @@ import './visualize_editor.scss'; import React, { RefObject } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiScreenReaderOnly } from '@elastic/eui'; +import { AppMountParameters } from 'kibana/public'; import { VisualizeTopNav } from './visualize_top_nav'; import { ExperimentalVisInfo } from './experimental_vis_info'; import { @@ -38,6 +39,7 @@ interface VisualizeEditorCommonProps { setHasUnsavedChanges: (value: boolean) => void; hasUnappliedChanges: boolean; isEmbeddableRendered: boolean; + onAppLeave: AppMountParameters['onAppLeave']; visEditorRef: RefObject; originatingApp?: string; setOriginatingApp?: (originatingApp: string | undefined) => void; @@ -54,6 +56,7 @@ export const VisualizeEditorCommon = ({ setHasUnsavedChanges, hasUnappliedChanges, isEmbeddableRendered, + onAppLeave, originatingApp, setOriginatingApp, visualizationIdFromUrl, @@ -76,10 +79,11 @@ export const VisualizeEditorCommon = ({ stateContainer={appState} visualizationIdFromUrl={visualizationIdFromUrl} embeddableId={embeddableId} + onAppLeave={onAppLeave} /> )} - {visInstance?.vis?.type?.isExperimental && } - {visInstance?.vis?.type?.getDeprecationMessage?.(visInstance.vis)} + {visInstance?.vis?.type?.stage === 'experimental' && } + {visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)} {visInstance && (

    diff --git a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx index dfd3c09f51ed5..b207529c456a1 100644 --- a/src/plugins/visualize/public/application/components/visualize_top_nav.tsx +++ b/src/plugins/visualize/public/application/components/visualize_top_nav.tsx @@ -19,7 +19,9 @@ import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import { OverlayRef } from 'kibana/public'; +import { AppMountParameters, OverlayRef } from 'kibana/public'; +import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../kibana_react/public'; import { VisualizeServices, @@ -43,6 +45,7 @@ interface VisualizeTopNavProps { stateContainer: VisualizeAppStateContainer; visualizationIdFromUrl?: string; embeddableId?: string; + onAppLeave: AppMountParameters['onAppLeave']; } const TopNav = ({ @@ -58,10 +61,11 @@ const TopNav = ({ stateContainer, visualizationIdFromUrl, embeddableId, + onAppLeave, }: VisualizeTopNavProps) => { const { services } = useKibana(); const { TopNavMenu } = services.navigation.ui; - const { setHeaderActionMenu } = services; + const { setHeaderActionMenu, visualizeCapabilities } = services; const { embeddableHandler, vis } = visInstance; const [inspectorSession, setInspectorSession] = useState(); const openInspector = useCallback(() => { @@ -93,6 +97,7 @@ const TopNav = ({ visualizationIdFromUrl, stateTransfer, embeddableId, + onAppLeave, }, services ); @@ -111,6 +116,7 @@ const TopNav = ({ services, embeddableId, stateTransfer, + onAppLeave, ]); const [indexPattern, setIndexPattern] = useState(vis.data.indexPattern); const showDatePicker = () => { @@ -131,6 +137,33 @@ const TopNav = ({ }; }, [inspectorSession]); + useEffect(() => { + onAppLeave((actions) => { + // Confirm when the user has made any changes to an existing visualizations + // or when the user has configured something without saving + if ( + ((originatingApp && originatingApp === 'dashboards') || originatingApp === 'canvas') && + (hasUnappliedChanges || hasUnsavedChanges) + ) { + return actions.confirm( + i18n.translate('visualize.confirmModal.confirmTextDescription', { + defaultMessage: 'Leave Visualize editor with unsaved changes?', + }), + i18n.translate('visualize.confirmModal.title', { + defaultMessage: 'Unsaved changes', + }) + ); + } + return actions.default(); + }); + }, [ + onAppLeave, + hasUnappliedChanges, + hasUnsavedChanges, + visualizeCapabilities.save, + originatingApp, + ]); + useEffect(() => { if (!vis.data.indexPattern) { services.data.indexPatterns.getDefault().then((index) => { diff --git a/src/plugins/visualize/public/application/index.tsx b/src/plugins/visualize/public/application/index.tsx index 4bec244e6efc9..1067fe613e466 100644 --- a/src/plugins/visualize/public/application/index.tsx +++ b/src/plugins/visualize/public/application/index.tsx @@ -27,7 +27,10 @@ import { VisualizeApp } from './app'; import { VisualizeServices } from './types'; import { addHelpMenuToAppChrome, addBadgeToAppChrome } from './utils'; -export const renderApp = ({ element }: AppMountParameters, services: VisualizeServices) => { +export const renderApp = ( + { element, onAppLeave }: AppMountParameters, + services: VisualizeServices +) => { // add help link to visualize docs into app chrome menu addHelpMenuToAppChrome(services.chrome, services.docLinks); // add readonly badge if saving restricted @@ -39,7 +42,7 @@ export const renderApp = ({ element }: AppMountParameters, services: VisualizeSe - + diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index cb68a647cb81d..eadf404daf918 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { TopNavMenuData } from 'src/plugins/navigation/public'; +import { AppMountParameters } from 'kibana/public'; import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; import { showSaveModal, @@ -51,6 +52,7 @@ interface TopNavConfigParams { visualizationIdFromUrl?: string; stateTransfer: EmbeddableStateTransfer; embeddableId?: string; + onAppLeave: AppMountParameters['onAppLeave']; } export const getTopNavConfig = ( @@ -66,6 +68,7 @@ export const getTopNavConfig = ( visualizationIdFromUrl, stateTransfer, embeddableId, + onAppLeave, }: TopNavConfigParams, { application, @@ -174,6 +177,12 @@ export const getTopNavConfig = ( stateTransfer.navigateToWithEmbeddablePackage(originatingApp, { state }); }; + const navigateToOriginatingApp = () => { + if (originatingApp) { + application.navigateToApp(originatingApp); + } + }; + const topNavMenu: TopNavMenuData[] = [ { id: 'inspector', @@ -225,6 +234,31 @@ export const getTopNavConfig = ( // disable the Share button if no action specified disableButton: !share || !!embeddableId, }, + ...(originatingApp === 'dashboards' || originatingApp === 'canvas' + ? [ + { + id: 'cancel', + label: i18n.translate('visualize.topNavMenu.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + emphasize: false, + description: i18n.translate('visualize.topNavMenu.cancelButtonAriaLabel', { + defaultMessage: 'Return to the last app without saving changes', + }), + testId: 'visualizeCancelAndReturnButton', + tooltip() { + if (hasUnappliedChanges || hasUnsavedChanges) { + return i18n.translate('visualize.topNavMenu.cancelAndReturnButtonTooltip', { + defaultMessage: 'Discard your changes before finishing', + }); + } + }, + run: async () => { + return navigateToOriginatingApp(); + }, + }, + ] + : []), ...(visualizeCapabilities.save && !embeddableId ? [ { @@ -297,6 +331,9 @@ export const getTopNavConfig = ( /> ); const isSaveAsButton = anchorElement.classList.contains('saveAsButton'); + onAppLeave((actions) => { + return actions.default(); + }); if ( originatingApp === 'dashboards' && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && @@ -342,6 +379,9 @@ export const getTopNavConfig = ( confirmOverwrite: false, returnToOrigin: true, }; + onAppLeave((actions) => { + return actions.default(); + }); if ( originatingApp === 'dashboards' && dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables && diff --git a/src/setup_node_env/dist.js b/src/setup_node_env/dist.js new file mode 100644 index 0000000000000..dd3af8c48a30a --- /dev/null +++ b/src/setup_node_env/dist.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('./no_transpilation'); +require('./polyfill'); diff --git a/src/setup_node_env/polyfill.js b/src/setup_node_env/polyfill.js new file mode 100644 index 0000000000000..ad87354c83429 --- /dev/null +++ b/src/setup_node_env/polyfill.js @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('core-js/stable'); diff --git a/src/test_utils/public/enzyme_helpers.tsx b/src/test_utils/public/enzyme_helpers.tsx index a7bed2ad84956..ce4e7c7298734 100644 --- a/src/test_utils/public/enzyme_helpers.tsx +++ b/src/test_utils/public/enzyme_helpers.tsx @@ -18,13 +18,13 @@ */ /** - * Components using the react-intl module require access to the intl context. + * Components using the @kbn/i18n module require access to the intl context. * This is not available when mounting single components in Enzyme. * These helper functions aim to address that and wrap a valid, * intl context around them. */ -import { I18nProvider, InjectedIntl, intlShape } from '@kbn/i18n/react'; +import { I18nProvider, InjectedIntl, intlShape, __IntlProvider } from '@kbn/i18n/react'; import { mount, ReactWrapper, render, shallow } from 'enzyme'; import React, { ReactElement, ValidationMap } from 'react'; @@ -33,7 +33,7 @@ const { intl } = (mount(
    -).find('IntlProvider') as ReactWrapper<{}, {}, import('react-intl').IntlProvider>) +).find('IntlProvider') as ReactWrapper<{}, {}, __IntlProvider>) .instance() .getChildContext(); @@ -52,7 +52,7 @@ function getOptions(context = {}, childContextTypes: ValidationMap = {}, pr } /** - * When using React-Intl `injectIntl` on components, props.intl is required. + * When using @kbn/i18n `injectI18n` on components, props.intl is required. */ function nodeWithIntlProp(node: ReactElement): ReactElement { return React.cloneElement(node, { intl }); diff --git a/tasks/function_test_groups.js b/tasks/function_test_groups.js index d60f3ae53eecc..ac41b3f36be0f 100644 --- a/tasks/function_test_groups.js +++ b/tasks/function_test_groups.js @@ -43,6 +43,8 @@ const getDefaultArgs = (tag) => { '--debug', '--config', 'test/new_visualize_flow/config.js', + '--config', + 'test/security_functional/config.ts', ]; }; diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index 45a09be6c2deb..ae96f17fdceb9 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -28,12 +28,12 @@ export default function ({ getService }) { expect(resp.body.fields).to.eql(sortBy(resp.body.fields, 'name')); }; - describe('response', () => { + describe('fields_for_wildcard_route response', () => { before(() => esArchiver.load('index_patterns/basic_index')); after(() => esArchiver.unload('index_patterns/basic_index')); - it('returns a flattened version of the fields in es', () => - supertest + it('returns a flattened version of the fields in es', async () => { + await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index' }) .expect(200, { @@ -86,10 +86,12 @@ export default function ({ getService }) { }, ], }) - .then(ensureFieldsAreSorted)); + .then(ensureFieldsAreSorted); + }); - it('always returns a field for all passed meta fields', () => - supertest + // https://github.com/elastic/kibana/issues/79813 + it.skip('always returns a field for all passed meta fields', async () => { + await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: 'basic_index', @@ -168,14 +170,16 @@ export default function ({ getService }) { }, ], }) - .then(ensureFieldsAreSorted)); + .then(ensureFieldsAreSorted); + }); - it('returns 404 when the pattern does not exist', () => - supertest + it('returns 404 when the pattern does not exist', async () => { + await supertest .get('/api/index_patterns/_fields_for_wildcard') .query({ pattern: '[non-existing-pattern]its-invalid-*', }) - .expect(404)); + .expect(404); + }); }); } diff --git a/test/common/config.js b/test/common/config.js index 6a62151b12814..9d6d531ae4b37 100644 --- a/test/common/config.js +++ b/test/common/config.js @@ -48,6 +48,9 @@ export default function () { `--elasticsearch.username=${kibanaServerTestUser.username}`, `--elasticsearch.password=${kibanaServerTestUser.password}`, `--home.disableWelcomeScreen=true`, + // Needed for async search functional tests to introduce a delay + `--data.search.aggs.shardDelay.enabled=true`, + `--security.showInsecureClusterWarning=false`, '--telemetry.banner=false', '--telemetry.optIn=false', // These are *very* important to have them pointing to staging diff --git a/test/functional/apps/dashboard/edit_visualizations.js b/test/functional/apps/dashboard/edit_visualizations.js new file mode 100644 index 0000000000000..a9bd2e87bcad5 --- /dev/null +++ b/test/functional/apps/dashboard/edit_visualizations.js @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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'; + +export default function ({ getService, getPageObjects }) { + const PageObjects = getPageObjects(['dashboard', 'header', 'visualize', 'common', 'visEditor']); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardVisualizations = getService('dashboardVisualizations'); + + const originalMarkdownText = 'Original markdown text'; + const modifiedMarkdownText = 'Modified markdown text'; + + const createMarkdownVis = async (title) => { + await testSubjects.click('dashboardAddNewPanelButton'); + await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); + await PageObjects.visualize.clickMarkdownWidget(); + await PageObjects.visEditor.setMarkdownTxt(originalMarkdownText); + await PageObjects.visEditor.clickGo(); + await PageObjects.visualize.saveVisualizationExpectSuccess(title, { + saveAsNew: true, + redirectToOrigin: true, + }); + }; + + const editMarkdownVis = async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.visEditor.setMarkdownTxt(modifiedMarkdownText); + await PageObjects.visEditor.clickGo(); + }; + + describe('edit visualizations from dashboard', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + it('save button returns to dashboard after editing visualization with changes saved', async () => { + const title = 'test save'; + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await createMarkdownVis(title); + + await editMarkdownVis(); + await PageObjects.visualize.saveVisualizationAndReturn(); + + const markdownText = await testSubjects.find('markdownBody'); + expect(await markdownText.getVisibleText()).to.eql(modifiedMarkdownText); + }); + + it('cancel button returns to dashboard after editing visualization without saving', async () => { + const title = 'test cancel'; + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + + await createMarkdownVis(title); + + await editMarkdownVis(); + await PageObjects.visualize.cancelAndReturn(true); + + const markdownText = await testSubjects.find('markdownBody'); + expect(await markdownText.getVisibleText()).to.eql(originalMarkdownText); + }); + + it('cancel button returns to dashboard with no modal if there are no changes to apply', async () => { + await dashboardPanelActions.openContextMenu(); + await dashboardPanelActions.clickEdit(); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.visualize.cancelAndReturn(false); + + const markdownText = await testSubjects.find('markdownBody'); + expect(await markdownText.getVisibleText()).to.eql(originalMarkdownText); + }); + }); +} diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index de4b3df9c40ef..b62907dfe2c24 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -53,9 +53,11 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./embeddable_rendering')); loadTestFile(require.resolve('./create_and_add_embeddables')); loadTestFile(require.resolve('./edit_embeddable_redirects')); + loadTestFile(require.resolve('./edit_visualizations')); loadTestFile(require.resolve('./time_zones')); loadTestFile(require.resolve('./dashboard_options')); loadTestFile(require.resolve('./data_shared_attributes')); + loadTestFile(require.resolve('./share')); loadTestFile(require.resolve('./embed_mode')); loadTestFile(require.resolve('./dashboard_back_button')); loadTestFile(require.resolve('./dashboard_error_handling')); diff --git a/test/functional/apps/dashboard/share.ts b/test/functional/apps/dashboard/share.ts new file mode 100644 index 0000000000000..cc9c4786f0592 --- /dev/null +++ b/test/functional/apps/dashboard/share.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['dashboard', 'common', 'share']); + + describe('share dashboard', () => { + before(async () => { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.preserveCrossAppState(); + await PageObjects.dashboard.loadSavedDashboard('few panels'); + }); + + it('has "panels" state when sharing a snapshot', async () => { + await PageObjects.share.clickShareTopNavButton(); + const sharedUrl = await PageObjects.share.getSharedUrl(); + expect(sharedUrl).to.contain('panels'); + }); + }); +} diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index f0472fb5a3da5..d9cb09432b26f 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }) { const queryBar = getService('queryBar'); const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); - describe('discover tab', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/78689 + describe.skip('discover tab', function describeIndexTests() { this.tags('includeFirefox'); before(async function () { await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/home/_add_data.js b/test/functional/apps/home/_add_data.js index 6ba123e579f7a..401a5becceb30 100644 --- a/test/functional/apps/home/_add_data.js +++ b/test/functional/apps/home/_add_data.js @@ -28,7 +28,7 @@ export default function ({ getService, getPageObjects }) { await PageObjects.common.navigateToUrl('home', 'tutorial_directory', { useActualUrl: true }); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { - const tutorialExists = await PageObjects.home.doesSynopsisExist('netflow'); + const tutorialExists = await PageObjects.home.doesSynopsisExist('netflowlogs'); expect(tutorialExists).to.be(true); }); }); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 459f596b30256..cc229ef0c2e08 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -434,7 +434,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } } - async getBodyText() { + async getJsonBodyText() { if (await find.existsByCssSelector('a[id=rawdata-tab]', defaultFindTimeout)) { // Firefox has 3 tabs and requires navigation to see Raw output await find.clickByCssSelector('a[id=rawdata-tab]'); @@ -449,6 +449,11 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo } } + async getBodyText() { + const body = await find.byCssSelector('body'); + return await body.getVisibleText(); + } + /** * Helper to detect an OSS licensed Kibana * Useful for functional testing in cloud environment @@ -500,6 +505,16 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo async scrollKibanaBodyTop() { await browser.setScrollToById('kibana-body', 0, 0); } + + /** + * Dismiss Banner if available. + */ + async dismissBanner() { + if (await testSubjects.exists('global-banner-item')) { + const button = await find.byButtonText('Dismiss'); + await button.click(); + } + } } return new CommonPage(); diff --git a/test/functional/page_objects/error_page.ts b/test/functional/page_objects/error_page.ts index 332ce835d0b1c..bc256f55155df 100644 --- a/test/functional/page_objects/error_page.ts +++ b/test/functional/page_objects/error_page.ts @@ -26,17 +26,11 @@ export function ErrorPageProvider({ getPageObjects }: FtrProviderContext) { class ErrorPage { public async expectForbidden() { const messageText = await common.getBodyText(); - expect(messageText).to.eql( - JSON.stringify({ - statusCode: 403, - error: 'Forbidden', - message: 'Forbidden', - }) - ); + expect(messageText).to.contain('You do not have permission to access the requested page'); } public async expectNotFound() { - const messageText = await common.getBodyText(); + const messageText = await common.getJsonBodyText(); expect(messageText).to.eql( JSON.stringify({ statusCode: 404, diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 6d94c3e581d6c..9619c81370cd8 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -363,6 +363,20 @@ export function VisualizePageProvider({ getService, getPageObjects }: FtrProvide await header.waitUntilLoadingHasFinished(); await testSubjects.missingOrFail('visualizesaveAndReturnButton'); } + + public async cancelAndReturn(showConfirmModal: boolean) { + await header.waitUntilLoadingHasFinished(); + await testSubjects.existOrFail('visualizeCancelAndReturnButton'); + await testSubjects.click('visualizeCancelAndReturnButton'); + if (showConfirmModal) { + await retry.waitFor( + 'confirm modal to show', + async () => await testSubjects.exists('appLeaveConfirmModal') + ); + await testSubjects.exists('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + } + } } return new VisualizePage(); diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json index 87a1bc20920a4..0d6d0286c5a8f 100644 --- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json +++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.0.0", + "@elastic/eui": "29.3.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json index 8bbf6274bd15f..8efd2ee432415 100644 --- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json +++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.0.0", + "@elastic/eui": "29.3.0", "react": "^16.12.0", "typescript": "4.0.2" } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index c0d9a03d02c32..4405063e54c06 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -12,7 +12,7 @@ "build": "rm -rf './target' && tsc" }, "devDependencies": { - "@elastic/eui": "29.0.0", + "@elastic/eui": "29.3.0", "@kbn/plugin-helpers": "1.0.0", "react": "^16.12.0", "typescript": "4.0.2" diff --git a/test/scripts/checks/bundle_limits.sh b/test/scripts/checks/bundle_limits.sh new file mode 100755 index 0000000000000..10d9d9343fda4 --- /dev/null +++ b/test/scripts/checks/bundle_limits.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +node scripts/build_kibana_platform_plugins --validate-limits diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh index 3fd3d02de1304..289e64f66c89b 100755 --- a/test/scripts/jenkins_xpack_build_plugins.sh +++ b/test/scripts/jenkins_xpack_build_plugins.sh @@ -10,5 +10,6 @@ node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \ + --scan-dir "$XPACK_DIR/test/licensing_plugin/plugins" \ --workers 12 \ --verbose diff --git a/test/security_functional/config.ts b/test/security_functional/config.ts new file mode 100644 index 0000000000000..2a35d40678fd2 --- /dev/null +++ b/test/security_functional/config.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT 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 { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../functional/config')); + + return { + testFiles: [require.resolve('./index.ts')], + services: functionalConfig.get('services'), + pageObjects: functionalConfig.get('pageObjects'), + servers: functionalConfig.get('servers'), + esTestCluster: functionalConfig.get('esTestCluster'), + apps: {}, + esArchiver: { + directory: path.resolve(__dirname, '../functional/fixtures/es_archiver'), + }, + snapshots: { + directory: path.resolve(__dirname, 'snapshots'), + }, + junit: { + reportName: 'Security OSS Functional Tests', + }, + kbnTestServer: { + ...functionalConfig.get('kbnTestServer'), + serverArgs: [ + ...functionalConfig + .get('kbnTestServer.serverArgs') + .filter((arg: string) => !arg.startsWith('--security.showInsecureClusterWarning')), + '--security.showInsecureClusterWarning=true', + // Required to load new platform plugins via `--plugin-path` flag. + '--env.name=development', + ], + }, + }; +} diff --git a/test/security_functional/index.ts b/test/security_functional/index.ts new file mode 100644 index 0000000000000..8066a4eacf61a --- /dev/null +++ b/test/security_functional/index.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { FtrProviderContext } from '../functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Security OSS', function () { + this.tags(['skipCloud', 'ciGroup2']); + loadTestFile(require.resolve('./insecure_cluster_warning')); + }); +} diff --git a/test/security_functional/insecure_cluster_warning.ts b/test/security_functional/insecure_cluster_warning.ts new file mode 100644 index 0000000000000..03d9d248d6790 --- /dev/null +++ b/test/security_functional/insecure_cluster_warning.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const pageObjects = getPageObjects(['common']); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + + describe('Insecure Cluster Warning', () => { + before(async () => { + await pageObjects.common.navigateToApp('home'); + await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); + // starting without user data + await esArchiver.unload('hamlet'); + }); + + after(async () => { + await esArchiver.unload('hamlet'); + }); + + describe('without user data', () => { + before(async () => { + await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); + await esArchiver.unload('hamlet'); + }); + + it('should not warn when the cluster contains no user data', async () => { + await browser.setLocalStorageItem( + 'insecureClusterWarningVisibility', + JSON.stringify({ show: false }) + ); + await pageObjects.common.navigateToApp('home'); + await testSubjects.missingOrFail('insecureClusterDefaultAlertText'); + }); + }); + + describe('with user data', () => { + before(async () => { + await pageObjects.common.navigateToApp('home'); + await browser.setLocalStorageItem('insecureClusterWarningVisibility', ''); + await esArchiver.load('hamlet'); + }); + + after(async () => { + await esArchiver.unload('hamlet'); + }); + + it('should warn about an insecure cluster, and hide when dismissed', async () => { + await pageObjects.common.navigateToApp('home'); + await testSubjects.existOrFail('insecureClusterDefaultAlertText'); + + await testSubjects.click('defaultDismissAlertButton'); + + await testSubjects.missingOrFail('insecureClusterDefaultAlertText'); + }); + + it('should not warn when local storage is configured to hide', async () => { + await browser.setLocalStorageItem( + 'insecureClusterWarningVisibility', + JSON.stringify({ show: false }) + ); + await pageObjects.common.navigateToApp('home'); + await testSubjects.missingOrFail('insecureClusterDefaultAlertText'); + }); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index fb57936248cf6..cf112b26a2cbb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,8 +13,8 @@ "src/**/__fixtures__/**/*", "src/test_utils/**/*", "src/core/**/*", - "src/plugins/kibana_utils/**/*", - "src/plugins/kibana_react/**/*" + "src/plugins/kibana_utils/**/*", + "src/plugins/kibana_react/**/*" // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index ec3dbd919fed6..fd5412c905683 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -87,15 +87,6 @@ def getLatestBuildInfo(comment) { return comment ? getBuildInfoFromComment(comment.body) : null } -def createBuildInfo() { - return [ - status: buildUtils.getBuildStatus(), - url: env.BUILD_URL, - number: env.BUILD_NUMBER, - commit: getCommitHash() - ] -} - def getHistoryText(builds) { if (!builds || builds.size() < 1) { return "" @@ -155,6 +146,16 @@ def getTestFailuresMessage() { return messages.join("\n") } +def getBuildStatusIncludingMetrics() { + def status = buildUtils.getBuildStatus() + + if (status == 'SUCCESS' && !ciStats.getMetricsSuccess()) { + return 'FAILURE' + } + + return status +} + def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { def info = previousCommentInfo ?: [:] info.builds = previousCommentInfo.builds ?: [] @@ -163,7 +164,10 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { info.builds = info.builds.findAll { it.number != env.BUILD_NUMBER } def messages = [] - def status = buildUtils.getBuildStatus() + + def status = isFinal + ? getBuildStatusIncludingMetrics() + : buildUtils.getBuildStatus() if (!isFinal) { def failuresPart = status != 'SUCCESS' ? ', with failures' : '' @@ -228,7 +232,12 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << "To update your PR or re-run it, just comment with:\n`@elasticmachine merge upstream`" - info.builds << createBuildInfo() + info.builds << [ + status: status, + url: env.BUILD_URL, + number: env.BUILD_NUMBER, + commit: getCommitHash() + ] messages << """