diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 915f0f799b210..27532f0f377f9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -60,7 +60,7 @@ /src/plugins/expressions/ @elastic/kibana-app-arch /src/plugins/inspector/ @elastic/kibana-app-arch /src/plugins/kibana_react/ @elastic/kibana-app-arch -/src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas +/src/plugins/kibana_react/public/code_editor @elastic/kibana-presentation /src/plugins/kibana_utils/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch /src/plugins/share/ @elastic/kibana-app-arch @@ -104,19 +104,19 @@ /x-pack/legacy/plugins/beats_management/ @elastic/beats #CC# /x-pack/plugins/beats_management/ @elastic/beats -# Canvas -/src/plugins/dashboard/ @elastic/kibana-canvas -/src/plugins/input_control_vis/ @elastic/kibana-canvas -/src/plugins/vis_type_markdown/ @elastic/kibana-canvas -/x-pack/plugins/canvas/ @elastic/kibana-canvas -/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-canvas -/x-pack/test/functional/apps/canvas/ @elastic/kibana-canvas -#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-canvas -#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-canvas -#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-canvas -#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-canvas -#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-canvas -#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-canvas +# Presentation +/src/plugins/dashboard/ @elastic/kibana-presentation +/src/plugins/input_control_vis/ @elastic/kibana-presentation +/src/plugins/vis_type_markdown/ @elastic/kibana-presentation +/x-pack/plugins/canvas/ @elastic/kibana-presentation +/x-pack/plugins/dashboard_enhanced/ @elastic/kibana-presentation +/x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation +#CC# /src/legacy/core_plugins/kibana/public/dashboard/ @elastic/kibana-presentation +#CC# /src/legacy/core_plugins/input_control_vis @elastic/kibana-presentation +#CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation +#CC# /x-pack/legacy/plugins/canvas/ @elastic/kibana-presentation +#CC# /x-pack/plugins/dashboard_mode @elastic/kibana-presentation +#CC# /x-pack/legacy/plugins/dashboard_mode/ @elastic/kibana-presentation # Core UI # Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index 42783808e32ed..a64ce33b8f976 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -13,7 +13,7 @@ assignees: '' **************************************** Please add a "NeededFor:${TeamName}" label to denote the team that is -requesting the breaking change is surfaced in the Upgrade Assistant. +requesting the breaking change to be surfaced in the Upgrade Assistant. --> @@ -27,21 +27,24 @@ requesting the breaking change is surfaced in the Upgrade Assistant. **How many users will be affected?** - - + + **What can users do to address the change manually?** - + **How could we make migration easier with the Upgrade Assistant?** + + **Are there any edge cases?** ## Test Data -Provide test data. We can’t build a solution without data to test it against. + ## Cross links -Cross-link to relevant [Elasticsearch breaking changes](https://www.elastic.co/guide/en/elasticsearch/reference/master/breaking-changes-8.0.html). \ No newline at end of file + \ No newline at end of file diff --git a/.github/workflows/pr-project-assigner.yml b/.github/workflows/pr-project-assigner.yml index ca5d0b9864f99..3ff3bb7fb97d1 100644 --- a/.github/workflows/pr-project-assigner.yml +++ b/.github/workflows/pr-project-assigner.yml @@ -18,4 +18,4 @@ jobs: # { "label": "Team:AppArch", "projectNumber": 37, "columnName": "Review in progress" }, # { "label": "Feature:Lens", "projectNumber": 32, "columnName": "In progress" }, -# { "label": "Team:Canvas", "projectNumber": 38, "columnName": "Review in progress" } \ No newline at end of file +# { "label": "Feature:Canvas", "projectNumber": 38, "columnName": "Review in progress" } diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index eb5827e121c74..96284345d1631 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -11,7 +11,7 @@ jobs: uses: elastic/github-actions/project-assigner@v2.0.0 id: project_assigner with: - issue-mappings: '[{"label": "Team:AppArch", "projectNumber": 37, "columnName": "To triage"}, {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Team:Canvas", "projectNumber": 38, "columnName": "Inbox"}]' + issue-mappings: '[{"label": "Team:AppArch", "projectNumber": 37, "columnName": "To triage"}, {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}]' ghToken: ${{ secrets.PROJECT_ASSIGNER_TOKEN }} diff --git a/.gitignore b/.gitignore index 0d9529eb65e54..45034583cffbb 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,21 @@ target .idea *.iml *.log + +# Ignore certain functional test runner artifacts /test/*/failure_debug /test/*/screenshots/diff /test/*/screenshots/failure /test/*/screenshots/session /test/*/screenshots/visual_regression_gallery.html + +# Ignore the same artifacts in x-pack +/x-pack/test/*/failure_debug +/x-pack/test/*/screenshots/diff +/x-pack/test/*/screenshots/failure +/x-pack/test/*/screenshots/session +/x-pack/test/*/screenshots/visual_regression_gallery.html + /html_docs .eslintcache /plugins/ diff --git a/docs/api/spaces-management/get_all.asciidoc b/docs/api/spaces-management/get_all.asciidoc index 8f7ba86f332de..e76848da80efb 100644 --- a/docs/api/spaces-management/get_all.asciidoc +++ b/docs/api/spaces-management/get_all.asciidoc @@ -11,6 +11,20 @@ experimental[] Retrieve all {kib} spaces. `GET :/api/spaces/space` +[[spaces-api-get-all-query-params]] +==== Query parameters + +`purpose`:: + (Optional, string) Valid options include `any`, `copySavedObjectsIntoSpace`, and `shareSavedObjectsIntoSpace`. This determines what + authorization checks are applied to the API call. If `purpose` is not provided in the URL, the `any` purpose is used. + +`include_authorized_purposes`:: + (Optional, boolean) When enabled, the API will return any spaces that the user is authorized to access in any capacity, and each space + will contain the purpose(s) for which the user is authorized. This can be useful to determine which spaces a user can read but not take a + specific action in. If the Security plugin is not enabled, this will have no effect, as no authorization checks would take place. ++ +NOTE: This option cannot be used in conjunction with `purpose`. + [[spaces-api-get-all-response-codes]] ==== Response code @@ -18,7 +32,17 @@ experimental[] Retrieve all {kib} spaces. Indicates a successful call. [[spaces-api-get-all-example]] -==== Example +==== Examples + +[[spaces-api-get-all-example-1]] +===== Default options + +Retrieve all spaces without specifying any options: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/spaces/space +-------------------------------------------------- The API returns the following: @@ -51,3 +75,63 @@ The API returns the following: } ] -------------------------------------------------- + +[[spaces-api-get-all-example-2]] +===== Custom options + +The user has read-only access to the Sales space. Retrieve all spaces and specify options: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/spaces/space?purpose=shareSavedObjectsIntoSpace&include_authorized_purposes=true +-------------------------------------------------- + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id": "default", + "name": "Default", + "description" : "This is the Default Space", + "disabledFeatures": [], + "imageUrl": "", + "_reserved": true, + "authorizedPurposes": { + "any": true, + "copySavedObjectsIntoSpace": true, + "findSavedObjects": true, + "shareSavedObjectsIntoSpace": true, + } + }, + { + "id": "marketing", + "name": "Marketing", + "description" : "This is the Marketing Space", + "color": "#aabbcc", + "disabledFeatures": ["apm"], + "initials": "MK", + "imageUrl": "data:image/png;base64,iVBORw0KGgoAAAANSU", + "authorizedPurposes": { + "any": true, + "copySavedObjectsIntoSpace": true, + "findSavedObjects": true, + "shareSavedObjectsIntoSpace": true, + } + }, + { + "id": "sales", + "name": "Sales", + "initials": "MK", + "disabledFeatures": ["discover", "timelion"], + "imageUrl": "", + "authorizedPurposes": { + "any": true, + "copySavedObjectsIntoSpace": false, + "findSavedObjects": true, + "shareSavedObjectsIntoSpace": false, + } + } +] +-------------------------------------------------- diff --git a/docs/developer/best-practices/typescript.asciidoc b/docs/developer/best-practices/typescript.asciidoc index 583a98f296de5..6d298f92b841e 100644 --- a/docs/developer/best-practices/typescript.asciidoc +++ b/docs/developer/best-practices/typescript.asciidoc @@ -32,7 +32,7 @@ Run `node scripts/find_plugins_without_ts_refs.js --id your_plugin_id` to get a [discrete] ==== Implementation -- Make sure all the plugins listed as dependencies in `kibana.json` file have migrated to TS project references. +- Make sure all the plugins listed as dependencies in *requiredPlugins*, *optionalPlugins* & *requiredBundles* properties of `kibana.json` manifest file have migrated to TS project references. - Add `tsconfig.json` in the root folder of your plugin. [source,json] ---- diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index e83e6d262f26c..c1ad859f0cb69 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -1,91 +1,128 @@ [[index-patterns]] == Create an index pattern -To explore and visualize data in {kib}, you must create an index pattern. -An index pattern tells {kib} which {es} indices contain the data that -you want to work with. -Once you create an index pattern, you're ready to: +{kib} requires an index pattern to access the {es} data that you want to explore. +An index pattern selects the data to use and allows you to define properties of the fields. -* Interactively explore your data in <>. -* Analyze your data in charts, tables, gauges, tag clouds, and more in <>. -* Show off your data in a <> workpad. -* If your data includes geo data, visualize it with <>. +An index pattern can point to a specific index, for example, your log data from yesterday, +or all indices that contain your data. It can also point to a +{ref}/data-streams.html[data stream] or {ref}/indices-aliases.html[index alias]. + +You’ll learn how to: + +* Create an index pattern +* Explore and configure the data fields +* Set the default index pattern +* Delete an index pattern [float] [[index-patterns-read-only-access]] -=== [xpack]#Read-only access# -If you have insufficient privileges to create or save index patterns, a read-only -indicator appears in Kibana. The buttons to create new index patterns or save -existing index patterns are not visible. For more information, see <>. +=== Before you begin -[role="screenshot"] -image::images/management-index-read-only-badge.png[Example of Index Pattern Management's read only access indicator in Kibana's header] +* To access *Index Patterns*, you must have the {kib} privilege +`Index Pattern Management`. To add the privilege, open the main menu, then click *Stack Management > Roles*. + +* If a read-only indicator appears in {kib}, you have insufficient privileges +to create or save index patterns. The buttons to create new index patterns or +save existing index patterns are not visible. For more information, +refer to <>. [float] [[settings-create-pattern]] === Create an index pattern -When you don't have an index pattern, {kib} prompts you to create one. Or, you can open the main menu, -then click *Stack Management > Index Patterns*. +If you collected data using one of the {kib} <>, uploaded a file, or added sample data, +you get an index pattern for free, and can start exploring your data. +If you loaded your own data, follow these steps to create an index pattern. + +. Open the main menu, then click to *Stack Management > Index Patterns*. +. Click *Create index pattern*. ++ [role="screenshot"] -image:management/index-patterns/images/rollup-index-pattern.png["Menu with rollup index pattern"] +image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] -[float] -==== Standard index pattern +. Start typing in the *Index pattern* field, and {kib} looks for the names of +{es} indices that match your input. +** Use a wildcard (*) to match multiple indices. +For example, suppose your system creates indices for Apache data +using the naming scheme `filebeat-apache-a`, `filebeat-apache-b`, and so on. +An index pattern named `filebeat-a` matches a single source, and `filebeat-*` matches multiple data sources. +Using a wildcard is the most popular approach. -Just start typing in the *Index pattern* field, and {kib} looks for -the names of {es} indices that match your input. Make sure that the name of the -index pattern is unique. +** Select multiple indices by entering multiple strings, +separated with a comma. Make sure there is no space after the comma. +For example, `filebeat-a,filebeat-b` matches two indices, but not other indices +you might have afterwards (filebeat-c). -[role="screenshot"] -image:management/index-patterns/images/create-index-pattern.png["Create index pattern"] +** Use a minus sign (-) to exclude an index, for example, test*,-test3. + +. Click *Next step*. -Your index pattern can match multiple {es} indices. -Use a comma to separate the names, with no space after the comma. The notation for -wildcards (`*`) and the ability to "exclude" (`-`) also apply -(for example, `test*,-test3`). +. If {kib} detects an index with a timestamp, expand the *Time field* menu, +and then specify the default field for filtering your data by time. ++ +If your index doesn’t have time-based data, or if you don’t want to select +the default timestamp field, choose *I don’t want to use the Time Filter*. ++ +NOTE: If you don’t set a default time field, you will not be able to use +global time filters on your dashboards. This is useful if +you have multiple time fields and want to create dashboards that combine visualizations +based on different timestamps. -If {kib} detects an index with a timestamp, you’re asked to choose a field to -filter your data by time. If you don’t specify a field, you won’t be able -to use the time filter. +. Click *Create index pattern*. ++ +{kib} is now configured to use your {es} data. + +. Select this index pattern when you search and visualize your data. [float] [[rollup-index-pattern]] -==== Rollup index pattern +==== Create an index pattern for rolled up data -If a rollup index is detected in the cluster, clicking *Create index pattern* -includes an item for creating a rollup index pattern. -You can match an index pattern to only rolled up data, or mix both rolled -up and raw data to explore and visualize all data together. -An index pattern can match -only one rollup index. When matching multiple indices, -use a comma to separate the names, with no space after the comma. +An index pattern can match one rollup index. For a combination rollup +index pattern with both raw and rolled up data, use the standard notation: -For specific fields, the data in a rollup index includes only summarized metrics. -From the original raw data, you are unable to search any other field. +```ts +rollup_logstash,kibana_sample_data_logs +``` +For an example, refer to <>. [float] [[management-cross-cluster-search]] -==== {ccs-cap} index pattern +==== Create an index pattern that searches across clusters + +If your {es} clusters are configured for {ref}/modules-cross-cluster-search.html[{ccs}], +you can create an index pattern to search across the clusters of your choosing. Use the +same syntax that you'd use in a raw {ccs} request in {es}: -If your {es} clusters are configured for {ref}/modules-cross-cluster-search.html[{ccs}], you can create -index patterns to search across the clusters of your choosing. Using the -same syntax that you'd use in a raw {ccs} request in {es}, create your -index pattern with the convention `:`. +```ts +: +``` For example, to query {ls} indices across two {es} clusters -that you set up for {ccs}, which are named `cluster_one` and `cluster_two`, -you would use `cluster_one:logstash-*,cluster_two:logstash-*` as your index pattern. +that you set up for {ccs}, named `cluster_one` and `cluster_two`, +use this for your index pattern: + +```ts + cluster_one:logstash-*,cluster_two:logstash-* +``` You can use wildcards in your cluster names -to match any number of clusters, so if you want to search {ls} indices across -clusters named `cluster_foo`, `cluster_bar`, and so on, you would use `cluster_*:logstash-*` -as your index pattern. +to match any number of clusters. For example, to search {ls} indices across +clusters named `cluster_foo`, `cluster_bar`, and so on, create this index pattern: + +```ts +cluster_*:logstash-* +``` To query across all {es} clusters that have been configured for {ccs}, use a standalone wildcard for your cluster name in your index -pattern: `*:logstash-*`. +pattern: + +```ts +*:logstash-* +``` Once an index pattern is configured using the {ccs} syntax, all searches and aggregations using that index pattern in {kib} take advantage of {ccs}. @@ -93,8 +130,74 @@ aggregations using that index pattern in {kib} take advantage of {ccs}. [float] [[reload-fields]] -=== Manage your index pattern +=== Explore and configure the data fields + +To explore and configure the data fields in your index pattern, open the main menu, then click +*Stack Management > Index Patterns*. Each field has a {ref}/mapping.html[mapping], +which indicates the type of data the field contains in {es}, +such as strings or boolean values. The field mapping also determines +how you can use the field, such as whether it can be searched or aggregated. + +[role="screenshot"] +image:management/index-patterns/images/new-index-pattern.png["Create index pattern"] + +[float] +==== Format the display of common field types + +Whenever possible, {kib} uses the same field type for display as +{es}. However, some field types that {es} supports are not available +in {kib}. Using field formatters, you can manually change the field type in {kib} to display your data the way you prefer +to see it, regardless of how it is stored in {es}. + +For example, if you store +date values in {es}, you can use a {kib} field formatter to change the display to mm/dd/yyyy format. +{kib} has field formatters for +<>, +<>, +<>, +and <>. + +A popularity counter keeps track of the fields you use most often. +The top five most popular fields and their values are displayed in <>. + +To edit the field format and popularity counter, click the edit icon +(image:management/index-patterns/images/edit_icon.png[]) in the index pattern detail view. + +[role="screenshot"] +image:management/index-patterns/images/edit-field-format.png["Edit field format"] + +[float] +==== Refresh the data fields + +To pick up newly-added fields, +refresh (image:management/index-patterns/images/refresh-icon.png[Refresh icon]) the index fields list. +This action also resets the {kib} popularity counters for the fields. + +[float] +[[default-index-pattern]] +=== Set the default index pattern + +The first index pattern you create is automatically designated as the default pattern, +but you can set any index pattern as the default. The default index pattern is automatically selected when you first open <> or create a visualization from scratch. + +. In *Index patterns*, click the index pattern name. +. Click the star icon (image:management/index-patterns/images/star.png[Star icon]). + +[float] +[[delete-index-pattern]] +=== Delete an index pattern + +This action removes the pattern from the list of saved objects in {kib}. +You will not be able to recover field formatters, scripted fields, source filters, +and field popularity data associated with the index pattern. Deleting an +index pattern does not remove any indices or data documents from {es}. + +WARNING: Deleting an index pattern breaks all visualizations, saved searches, and other saved objects that reference the pattern. + +. In *Index patterns*, click the index pattern name. +. Click the delete icon (image:management/index-patterns/images/delete.png[Delete icon]). + +[float] +=== What’s next -To drill down into the fields and associated data types in an index pattern, -click its name in the *Index patterns* overview page. -For more information, refer to <>. +* Learn about <> and how to create data on the fly. diff --git a/docs/management/index-patterns/images/delete.png b/docs/management/index-patterns/images/delete.png new file mode 100755 index 0000000000000..a5bb37368812b Binary files /dev/null and b/docs/management/index-patterns/images/delete.png differ diff --git a/docs/management/index-patterns/images/edit-field-format.png b/docs/management/index-patterns/images/edit-field-format.png new file mode 100755 index 0000000000000..15ab0c5bf8763 Binary files /dev/null and b/docs/management/index-patterns/images/edit-field-format.png differ diff --git a/docs/management/index-patterns/images/index-pattern-ui.png b/docs/management/index-patterns/images/index-pattern-ui.png deleted file mode 100644 index 7d16540aa03a2..0000000000000 Binary files a/docs/management/index-patterns/images/index-pattern-ui.png and /dev/null differ diff --git a/docs/management/index-patterns/images/refresh-icon.png b/docs/management/index-patterns/images/refresh-icon.png new file mode 100755 index 0000000000000..00d1a4c7653de Binary files /dev/null and b/docs/management/index-patterns/images/refresh-icon.png differ diff --git a/docs/management/index-patterns/images/rollup-index-pattern.png b/docs/management/index-patterns/images/rollup-index-pattern.png deleted file mode 100644 index d624f1112533a..0000000000000 Binary files a/docs/management/index-patterns/images/rollup-index-pattern.png and /dev/null differ diff --git a/docs/management/index-patterns/images/star.png b/docs/management/index-patterns/images/star.png new file mode 100755 index 0000000000000..f35408d1c3ee1 Binary files /dev/null and b/docs/management/index-patterns/images/star.png differ diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 503f97aabd13f..8933bf64d2736 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -132,7 +132,7 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> +a| <> |Create and manage the index patterns that retrieve your data from {es}. | <> diff --git a/package.json b/package.json index 4138ad5c2ad0b..44a0c833eae27 100644 --- a/package.json +++ b/package.json @@ -367,14 +367,14 @@ "@kbn/expect": "link:packages/kbn-expect", "@kbn/optimizer": "link:packages/kbn-optimizer", "@kbn/plugin-generator": "link:packages/kbn-plugin-generator", + "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", "@kbn/pm": "link:packages/kbn-pm", "@kbn/release-notes": "link:packages/kbn-release-notes", - "@kbn/plugin-helpers": "link:packages/kbn-plugin-helpers", + "@kbn/storybook": "link:packages/kbn-storybook", "@kbn/telemetry-tools": "link:packages/kbn-telemetry-tools", "@kbn/test": "link:packages/kbn-test", "@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector", "@kbn/utility-types": "link:packages/kbn-utility-types", - "@kbn/storybook": "link:packages/kbn-storybook", "@mapbox/geojson-rewind": "^0.5.0", "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", @@ -447,15 +447,17 @@ "@types/glob": "^7.1.2", "@types/globby": "^8.0.0", "@types/graphql": "^0.13.2", + "@types/gulp": "^4.0.6", + "@types/gulp-zip": "^4.0.1", + "@types/hapi": "^17.0.18", + "@types/hapi-auth-cookie": "^9.1.0", "@types/hapi__boom": "^7.4.1", - "@types/hapi": "^17.0.18", "@types/hapi__cookie": "^10.1.1", - "@types/hapi-auth-cookie": "^9.1.0", "@types/hapi__h2o2": "8.3.0", + "@types/hapi__cookie": "^10.1.1", + "@types/hapi__h2o2": "8.3.0", "@types/hapi__hapi": "^18.2.6", "@types/hapi__hoek": "^6.2.0", "@types/hapi__inert": "^5.2.1", "@types/hapi__podium": "^3.4.1", - "@types/gulp": "^4.0.6", - "@types/gulp-zip": "^4.0.1", "@types/hapi__wreck": "^15.0.1", "@types/has-ansi": "^3.0.0", "@types/he": "^1.1.1", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 98385b49dafa9..30b1b8ebf17e2 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -34,7 +34,7 @@ export { KBN_P12_PATH, KBN_P12_PASSWORD, } from './certs'; -export { KbnClient } from './kbn_client'; +export * from './kbn_client'; export * from './run'; export * from './axios'; export * from './stdio'; diff --git a/packages/kbn-dev-utils/src/kbn_client/index.ts b/packages/kbn-dev-utils/src/kbn_client/index.ts index 72214b6c61746..47ef47143a6d8 100644 --- a/packages/kbn-dev-utils/src/kbn_client/index.ts +++ b/packages/kbn-dev-utils/src/kbn_client/index.ts @@ -17,5 +17,5 @@ * under the License. */ -export { KbnClient } from './kbn_client'; +export * from './kbn_client'; export { uriencode } from './kbn_client_requester'; diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts index 7184727fc53de..6539f5b50b56b 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client.ts @@ -18,37 +18,55 @@ */ import { ToolingLog } from '../tooling_log'; -import { KibanaConfig, KbnClientRequester, ReqOptions } from './kbn_client_requester'; +import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; import { KbnClientStatus } from './kbn_client_status'; import { KbnClientPlugins } from './kbn_client_plugins'; import { KbnClientVersion } from './kbn_client_version'; import { KbnClientSavedObjects } from './kbn_client_saved_objects'; import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; +export interface KbnClientOptions { + url: string; + certificateAuthorities?: Buffer[]; + log: ToolingLog; + uiSettingDefaults?: UiSettingValues; +} + export class KbnClient { - private readonly requester = new KbnClientRequester(this.log, this.kibanaConfig); - readonly status = new KbnClientStatus(this.requester); - readonly plugins = new KbnClientPlugins(this.status); - readonly version = new KbnClientVersion(this.status); - readonly savedObjects = new KbnClientSavedObjects(this.log, this.requester); - readonly uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); + readonly status: KbnClientStatus; + readonly plugins: KbnClientPlugins; + readonly version: KbnClientVersion; + readonly savedObjects: KbnClientSavedObjects; + readonly uiSettings: KbnClientUiSettings; + + private readonly requester: KbnClientRequester; + private readonly log: ToolingLog; + private readonly uiSettingDefaults?: UiSettingValues; /** * Basic Kibana server client that implements common behaviors for talking * to the Kibana server from dev tooling. - * - * @param log ToolingLog - * @param kibanaUrls Array of kibana server urls to send requests to - * @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets */ - constructor( - private readonly log: ToolingLog, - private readonly kibanaConfig: KibanaConfig, - private readonly uiSettingDefaults?: UiSettingValues - ) { - if (!kibanaConfig.url) { - throw new Error('missing Kibana urls'); + constructor(options: KbnClientOptions) { + if (!options.url) { + throw new Error('missing Kibana url'); } + if (!options.log) { + throw new Error('missing ToolingLog'); + } + + this.log = options.log; + this.uiSettingDefaults = options.uiSettingDefaults; + + this.requester = new KbnClientRequester(this.log, { + url: options.url, + certificateAuthorities: options.certificateAuthorities, + }); + this.status = new KbnClientStatus(this.requester); + this.plugins = new KbnClientPlugins(this.status); + this.version = new KbnClientVersion(this.status); + this.savedObjects = new KbnClientSavedObjects(this.log, this.requester); + this.uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); } /** diff --git a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts index 2aba2be56f277..dad0854e51b44 100644 --- a/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts +++ b/packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts @@ -69,31 +69,27 @@ const delay = (ms: number) => setTimeout(resolve, ms); }); -export interface KibanaConfig { +interface Options { url: string; - ssl?: { - enabled: boolean; - key: string; - certificate: string; - certificateAuthorities: string; - }; + certificateAuthorities?: Buffer[]; } export class KbnClientRequester { + private readonly url: string; private readonly httpsAgent: Https.Agent | null; - constructor(private readonly log: ToolingLog, private readonly kibanaConfig: KibanaConfig) { + + constructor(private readonly log: ToolingLog, options: Options) { + this.url = options.url; this.httpsAgent = - kibanaConfig.ssl && kibanaConfig.ssl.enabled + Url.parse(options.url).protocol === 'https:' ? new Https.Agent({ - cert: kibanaConfig.ssl.certificate, - key: kibanaConfig.ssl.key, - ca: kibanaConfig.ssl.certificateAuthorities, + ca: options.certificateAuthorities, }) : null; } private pickUrl() { - return this.kibanaConfig.url; + return this.url; } public resolveUrl(relativeUrl: string = '/') { @@ -132,7 +128,7 @@ export class KbnClientRequester { errorMessage = `Conflict on GET (path=${options.path}, attempt=${attempt}/${maxAttempts})`; this.log.error(errorMessage); } else if (requestedRetries || failedToGetResponse) { - errorMessage = `[${description}] request failed (attempt=${attempt}/${maxAttempts})`; + errorMessage = `[${description}] request failed (attempt=${attempt}/${maxAttempts}): ${error.message}`; this.log.error(errorMessage); } else { throw error; diff --git a/packages/kbn-es-archiver/src/es_archiver.ts b/packages/kbn-es-archiver/src/es_archiver.ts index d61e7d2a422e8..c6f890b963e3d 100644 --- a/packages/kbn-es-archiver/src/es_archiver.ts +++ b/packages/kbn-es-archiver/src/es_archiver.ts @@ -49,7 +49,7 @@ export class EsArchiver { this.client = client; this.dataDir = dataDir; this.log = log; - this.kbnClient = new KbnClient(log, { url: kibanaUrl }); + this.kbnClient = new KbnClient({ log, url: kibanaUrl }); } /** diff --git a/packages/kbn-release-notes/src/release_notes_config.ts b/packages/kbn-release-notes/src/release_notes_config.ts index 88ab5dfa2fda4..f0e7e76cb4612 100644 --- a/packages/kbn-release-notes/src/release_notes_config.ts +++ b/packages/kbn-release-notes/src/release_notes_config.ts @@ -119,10 +119,6 @@ export const AREAS: Area[] = [ title: 'Maps', labels: ['Team:Geo'], }, - { - title: 'Canvas', - labels: ['Team:Canvas'], - }, { title: 'QA', labels: ['Team:QA'], @@ -138,6 +134,10 @@ export const AREAS: Area[] = [ 'Feature:Security/Feature Controls', ], }, + { + title: 'Canvas', + labels: ['Feature:Canvas'], + }, { title: 'Dashboard', labels: ['Feature:Dashboard', 'Feature:Drilldowns'], diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 701171876ad2c..6ed114d62e244 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -38,14 +38,7 @@ const urlPartsSchema = () => password: Joi.string(), pathname: Joi.string().regex(/^\//, 'start with a /'), hash: Joi.string().regex(/^\//, 'start with a /'), - ssl: Joi.object() - .keys({ - enabled: Joi.boolean().default(false), - certificate: Joi.string().optional(), - certificateAuthorities: Joi.string().optional(), - key: Joi.string().optional(), - }) - .default(), + certificateAuthorities: Joi.array().items(Joi.binary()).optional(), }) .default(); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 629bf97c24887..6988a3211fa12 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -93,7 +93,7 @@ export class DocLinksService { max: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-max-aggregation.html`, median: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-percentile-aggregation.html`, min: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-min-aggregation.html`, - moving_avg: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-movavg-aggregation.html`, + moving_avg: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-movfn-aggregation.html`, percentile_ranks: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-percentile-rank-aggregation.html`, serial_diff: `${ELASTICSEARCH_DOCS}search-aggregations-pipeline-serialdiff-aggregation.html`, std_dev: `${ELASTICSEARCH_DOCS}search-aggregations-metrics-extendedstats-aggregation.html`, 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 bd30efcb1c6d3..c47edfb9cf63d 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -43,7 +43,17 @@ RUN chmod -R g=u /usr/share/kibana FROM {{{baseOSImage}}} EXPOSE 5601 +{{#ubi}} + # https://github.com/rpm-software-management/microdnf/issues/50 + RUN mkdir -p /run/user/$(id -u) + + # crypto-policies not currently compatible with libnss :sadpanda: + RUN printf "[main]\nexclude=crypto-policies" > /etc/dnf/dnf.conf +{{/ubi}} + RUN for iter in {1..10}; do \ + # update microdnf to have exclusion feature for dnf configuration + {{packageManager}} update microdnf --setopt=tsflags=nodocs -y && \ {{packageManager}} update --setopt=tsflags=nodocs -y && \ {{packageManager}} install --setopt=tsflags=nodocs -y \ fontconfig freetype shadow-utils libnss3.so {{#ubi}}findutils{{/ubi}} && \ diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index c588377d83c46..dc49909bd1a3a 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -61,8 +61,13 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: // side code entries that were provided const serverDependencies = await getDependencies(baseDir, serverEntries); + // List of hardcoded dependencies that we need and that are not discovered + // searching through code imports + // TODO: remove this once we get rid off @kbn/ui-framework + const hardCodedDependencies = ['@kbn/ui-framework']; + // Consider this as our whiteList for the modules we can't delete - const whiteListedModules = [...serverDependencies]; + const whiteListedModules = [...serverDependencies, ...hardCodedDependencies]; const listedDependencies = Object.keys(listedPkgDependencies); const filteredListedDependencies = listedDependencies.filter((entry) => { diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 35ee1d82d8ec4..6023cd133d763 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -82,7 +82,13 @@ import { getDashboardTitle } from './dashboard_strings'; import { DashboardAppScope } from './dashboard_app'; import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters'; import { RenderDeps } from './application'; -import { IKbnUrlStateStorage, setStateToKbnUrl, unhashUrl } from '../../../kibana_utils/public'; +import { + IKbnUrlStateStorage, + removeQueryParam, + setStateToKbnUrl, + unhashUrl, + getQueryParams, +} from '../../../kibana_utils/public'; import { addFatalError, AngularHttpError, @@ -121,6 +127,9 @@ interface UrlParamValues extends Omit + getQueryParams(history.location)[DashboardConstants.SEARCH_SESSION_ID] as string | undefined; + export class DashboardAppController { // Part of the exposed plugin API - do not remove without careful consideration. appStatus: { @@ -420,7 +429,11 @@ export class DashboardAppController { >(DASHBOARD_CONTAINER_TYPE); if (dashboardFactory) { - const searchSessionId = searchService.session.start(); + const searchSessionIdFromURL = getSearchSessionIdFromURL(history); + if (searchSessionIdFromURL) { + searchService.session.restore(searchSessionIdFromURL); + } + const searchSessionId = searchSessionIdFromURL ?? searchService.session.start(); dashboardFactory .create({ ...getDashboardInput(), searchSessionId }) .then((container: DashboardContainer | ErrorEmbeddable | undefined) => { @@ -599,8 +612,15 @@ export class DashboardAppController { const refreshDashboardContainer = () => { const changes = getChangesFromAppStateForContainerState(); if (changes && dashboardContainer) { - const searchSessionId = searchService.session.start(); - dashboardContainer.updateInput({ ...changes, searchSessionId }); + if (getSearchSessionIdFromURL(history)) { + // going away from a background search results + removeQueryParam(history, DashboardConstants.SEARCH_SESSION_ID, true); + } + + dashboardContainer.updateInput({ + ...changes, + searchSessionId: searchService.session.start(), + }); } }; diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap index 94a9c646a403c..d68011d2f7fde 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.js.snap @@ -31,7 +31,9 @@ exports[`after fetch hideWriteControls 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { @@ -134,7 +136,9 @@ exports[`after fetch initialFilter 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { @@ -237,7 +241,9 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { @@ -340,7 +346,9 @@ exports[`after fetch renders table rows 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { @@ -443,7 +451,9 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { @@ -545,7 +555,9 @@ exports[`renders empty page in before initial fetch to avoid flickering 1`] = ` /> } + rowHeader="title" searchFilters={Array []} + tableCaption="Dashboards" tableColumns={ Array [ Object { diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.js index 31e5bcf83150b..1af89f4bcb71f 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.js +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.js @@ -43,6 +43,7 @@ export class DashboardListing extends React.Component { { ); }); + test('searchSessionId', async () => { + const generator = createDashboardUrlGenerator(() => + Promise.resolve({ + appBasePath: APP_BASE_PATH, + useHashedUrl: false, + savedDashboardLoader: createMockDashboardLoader(), + }) + ); + const url = await generator.createUrl!({ + timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, + dashboardId: '123', + filters: [], + query: { query: 'bye', language: 'kuery' }, + searchSessionId: '__sessionSearchId__', + }); + expect(url).toMatchInlineSnapshot( + `"xyz/app/dashboards#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__"` + ); + }); + test('if no useHash setting is given, uses the one was start services', async () => { const generator = createDashboardUrlGenerator(() => Promise.resolve({ diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 68a50396e00d6..b23b26e4022dd 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -29,6 +29,7 @@ import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition } from '../../share/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { ViewMode } from '../../embeddable/public'; +import { DashboardConstants } from './dashboard_constants'; export const STATE_STORAGE_KEY = '_a'; export const GLOBAL_STATE_STORAGE_KEY = '_g'; @@ -79,6 +80,12 @@ export interface DashboardUrlGeneratorState { * View mode of the dashboard. */ viewMode?: ViewMode; + + /** + * Search search session ID to restore. + * (Background search) + */ + searchSessionId?: string; } export const createDashboardUrlGenerator = ( @@ -124,7 +131,7 @@ export const createDashboardUrlGenerator = ( ...state.filters, ]; - const appStateUrl = setStateToKbnUrl( + let url = setStateToKbnUrl( STATE_STORAGE_KEY, cleanEmptyKeys({ query: state.query, @@ -135,7 +142,7 @@ export const createDashboardUrlGenerator = ( `${appBasePath}#/${hash}` ); - return setStateToKbnUrl( + url = setStateToKbnUrl( GLOBAL_STATE_STORAGE_KEY, cleanEmptyKeys({ time: state.timeRange, @@ -143,7 +150,13 @@ export const createDashboardUrlGenerator = ( refreshInterval: state.refreshInterval, }), { useHash }, - appStateUrl + url ); + + if (state.searchSessionId) { + url = `${url}&${DashboardConstants.SEARCH_SESSION_ID}=${state.searchSessionId}`; + } + + return url; }, }); diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 67c93ad8a406c..f65740d0d4e7d 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -12,7 +12,6 @@ "urlForwarding", "navigation", "uiActions", - "visualizations", "savedObjects" ], "optionalPlugins": ["home", "share"], diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index ebd086dd1e38a..389eda90e00a1 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -65,7 +65,6 @@ const { timefilter, toastNotifications, uiSettings: config, - visualizations, } = getServices(); import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; @@ -874,11 +873,11 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp }); if (getTimeField()) { - const tabifiedData = tabifyAggResponse($scope.vis.data.aggs, resp); + const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp); $scope.searchSource.rawResponse = resp; $scope.histogramData = discoverResponseHandler( tabifiedData, - getDimensions($scope.vis.data.aggs.aggs, $scope.timeRange) + getDimensions($scope.opts.chartAggConfigs.aggs, $scope.timeRange) ); $scope.updateTime(); } @@ -1045,27 +1044,19 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise }, }, ]; - - $scope.vis = await visualizations.createVis('histogram', { - title: savedSearch.title, - params: { - addLegend: false, - addTimeMarker: true, - }, - data: { - aggs: visStateAggs, - searchSource: $scope.searchSource.getSerializedFields(), - }, - }); + $scope.opts.chartAggConfigs = data.search.aggs.createAggConfigs( + $scope.indexPattern, + visStateAggs + ); $scope.searchSource.onRequestStart((searchSource, options) => { - if (!$scope.vis) return; - return $scope.vis.data.aggs.onSearchRequestStart(searchSource, options); + if (!$scope.opts.chartAggConfigs) return; + return $scope.opts.chartAggConfigs.onSearchRequestStart(searchSource, options); }); $scope.searchSource.setField('aggs', function () { - if (!$scope.vis) return; - return $scope.vis.data.aggs.toDsl(); + if (!$scope.opts.chartAggConfigs) return; + return $scope.opts.chartAggConfigs.toDsl(); }); } diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 8582f71c0cb88..7cdcd6cbbca3a 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -30,7 +30,6 @@ top-nav-menu="topNavMenu" update-query="handleRefresh" update-saved-query-id="updateSavedQueryId" - vis="vis" > diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts index a3502cbb211fa..cb3cb06aa90a3 100644 --- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts @@ -51,6 +51,5 @@ export function createDiscoverLegacyDirective(reactDirective: any) { ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], ['updateSavedQueryId', { watchDepth: 'reference' }], - ['vis', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 3ca421f809640..5d419977113a8 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -39,13 +39,13 @@ import { Query, IndexPatternAttributes, DataPublicPluginStart, + AggConfigs, } from '../../../../data/public'; import { Chart } from '../angular/helpers/point_series'; import { AppState } from '../angular/discover_state'; import { SavedSearch } from '../../saved_searches'; import { SavedObject } from '../../../../../core/types'; -import { Vis } from '../../../../visualizations/public'; import { TopNavMenuData } from '../../../../navigation/public'; export interface DiscoverLegacyProps { @@ -66,14 +66,15 @@ export interface DiscoverLegacyProps { onSkipBottomButtonClick: () => void; onSort: (sort: string[][]) => void; opts: { - savedSearch: SavedSearch; + chartAggConfigs?: AggConfigs; config: IUiSettingsClient; + data: DataPublicPluginStart; + fixedScroll: (el: HTMLElement) => void; indexPatternList: Array>; - timefield: string; sampleSize: number; - fixedScroll: (el: HTMLElement) => void; + savedSearch: SavedSearch; setHeaderActionMenu: (menuMount: MountPoint | undefined) => void; - data: DataPublicPluginStart; + timefield: string; }; resetQuery: () => void; resultState: string; @@ -87,7 +88,6 @@ export interface DiscoverLegacyProps { topNavMenu: TopNavMenuData[]; updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void; updateSavedQueryId: (savedQueryId?: string) => void; - vis?: Vis; } export function DiscoverLegacy({ @@ -119,12 +119,11 @@ export function DiscoverLegacy({ topNavMenu, updateQuery, updateSavedQueryId, - vis, }: DiscoverLegacyProps) { const [isSidebarClosed, setIsSidebarClosed] = useState(false); const { TopNavMenu } = getServices().navigation.ui; const { savedSearch, indexPatternList } = opts; - const bucketAggConfig = vis?.data?.aggs?.aggs[1]; + const bucketAggConfig = opts.chartAggConfigs?.aggs[1]; const bucketInterval = bucketAggConfig && search.aggs.isDateHistogramBucketAggConfig(bucketAggConfig) ? bucketAggConfig.buckets?.getInterval() @@ -242,7 +241,7 @@ export function DiscoverLegacy({ })} className="dscTimechart" > - {vis && rows.length !== 0 && ( + {opts.chartAggConfigs && rows.length !== 0 && (
Promise; getEmbeddableInjector: any; uiSettings: IUiSettingsClient; - visualizations: VisualizationsStart; } export async function buildServices( @@ -108,6 +106,5 @@ export async function buildServices( timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, - visualizations: plugins.visualizations, }; } diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 11ec4f08d9514..10bde12f8768d 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -35,7 +35,6 @@ import { EmbeddableStart, EmbeddableSetup } from 'src/plugins/embeddable/public' import { ChartsPluginStart } from 'src/plugins/charts/public'; import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; -import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public'; import { KibanaLegacySetup, KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; @@ -123,7 +122,6 @@ export interface DiscoverSetupPlugins { kibanaLegacy: KibanaLegacySetup; urlForwarding: UrlForwardingSetup; home?: HomePublicPluginSetup; - visualizations: VisualizationsSetup; data: DataPublicPluginSetup; } @@ -140,7 +138,6 @@ export interface DiscoverStartPlugins { kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; - visualizations: VisualizationsStart; savedObjects: SavedObjectsStart; } diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 5e6ac6df642fd..4f509876a75f4 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -66,6 +66,14 @@ export interface TableListViewProps { * If the table is not empty, this component renders its own h1 element using the same id. */ headingId?: string; + /** + * Indicates which column should be used as the identifying cell in each row. + */ + rowHeader: string; + /** + * Describes the content of the table. If not specified, the caption will be "This table contains {itemCount} rows." + */ + tableCaption: string; searchFilters?: SearchFilterConfig[]; } @@ -471,6 +479,8 @@ class TableListView extends React.Component ); } diff --git a/src/plugins/kibana_utils/public/history/get_query_params.test.ts b/src/plugins/kibana_utils/public/history/get_query_params.test.ts new file mode 100644 index 0000000000000..dcdf796c04dbb --- /dev/null +++ b/src/plugins/kibana_utils/public/history/get_query_params.test.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 { getQueryParams } from './get_query_params'; +import { Location } from 'history'; + +describe('getQueryParams', () => { + it('should getQueryParams', () => { + const location: Location = { + pathname: '/dashboard/c3a76790-3134-11ea-b024-83a7b4783735', + search: "?_a=(description:'')&_b=3", + state: null, + hash: '', + }; + + const query = getQueryParams(location); + + expect(query).toMatchInlineSnapshot(` + Object { + "_a": "(description:'')", + "_b": "3", + } + `); + }); +}); diff --git a/src/plugins/kibana_utils/public/history/get_query_params.ts b/src/plugins/kibana_utils/public/history/get_query_params.ts new file mode 100644 index 0000000000000..e28aafd2d3be8 --- /dev/null +++ b/src/plugins/kibana_utils/public/history/get_query_params.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 { parse, ParsedQuery } from 'query-string'; +import { Location } from 'history'; + +export function getQueryParams(location: Location): ParsedQuery { + const search = (location.search || '').replace(/^\?/, ''); + const query = parse(search, { sort: false }); + return query; +} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index bb13ea09f928a..88693a971250b 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,3 +19,4 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; +export { getQueryParams } from './get_query_params'; diff --git a/src/plugins/kibana_utils/public/history/remove_query_param.ts b/src/plugins/kibana_utils/public/history/remove_query_param.ts index bf945e5b064aa..d3491057d24e9 100644 --- a/src/plugins/kibana_utils/public/history/remove_query_param.ts +++ b/src/plugins/kibana_utils/public/history/remove_query_param.ts @@ -17,14 +17,14 @@ * under the License. */ -import { parse, stringify } from 'query-string'; +import { stringify } from 'query-string'; import { History, Location } from 'history'; import { url } from '../../common'; +import { getQueryParams } from './get_query_params'; export function removeQueryParam(history: History, param: string, replace: boolean = true) { const oldLocation = history.location; - const search = (oldLocation.search || '').replace(/^\?/, ''); - const query = parse(search, { sort: false }); + const query = getQueryParams(oldLocation); delete query[param]; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 7edf62ce04e81..9ba42d39139da 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StopSyncStateFnType, } from './state_sync'; export { Configurable, CollectConfigProps } from './ui'; -export { removeQueryParam, redirectWhenMissing } from './history'; +export { removeQueryParam, redirectWhenMissing, getQueryParams } from './history'; export { applyDiff } from './state_management/utils/diff_object'; export { createStartServicesGetter, StartServicesGetter } from './core/create_start_service_getter'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 139f9f2e8703d..fd90663e4700d 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -266,8 +266,19 @@ exports[`SavedObjectsTable should render normally 1`] = ` "serverBasePath": "", } } - canDelete={false} canGoInApp={[Function]} + capabilities={ + Object { + "catalogue": Object {}, + "management": Object {}, + "navLinks": Object {}, + "savedObjectsManagement": Object { + "delete": false, + "edit": false, + "read": true, + }, + } + } columnRegistry={ Object { "getAll": [MockFunction], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx index 7733a587ca9a7..f83536d2c5ea0 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx @@ -24,7 +24,6 @@ import { keys } from '@elastic/eui'; import { httpServiceMock } from '../../../../../../core/public/mocks'; import { actionServiceMock } from '../../../services/action_service.mock'; import { columnServiceMock } from '../../../services/column_service.mock'; -import { SavedObjectsManagementAction } from '../../..'; import { Table, TableProps } from './table'; const defaultProps: TableProps = { @@ -82,7 +81,7 @@ const defaultProps: TableProps = { onTableChange: () => {}, isSearching: false, onShowRelationships: () => {}, - canDelete: true, + capabilities: { savedObjectsManagement: { delete: true } } as any, }; describe('Table', () => { @@ -121,7 +120,11 @@ describe('Table', () => { { type: 'search' }, { type: 'index-pattern' }, ] as any; - const customizedProps = { ...defaultProps, selectedSavedObjects, canDelete: false }; + const customizedProps = { + ...defaultProps, + selectedSavedObjects, + capabilities: { savedObjectsManagement: { delete: false } } as any, + }; const component = shallowWithI18nProvider(); expect(component).toMatchSnapshot(); @@ -137,7 +140,8 @@ describe('Table', () => { refreshOnFinish: () => true, euiAction: { name: 'foo', description: 'bar', icon: 'beaker', type: 'icon' }, registerOnFinishCallback: (callback: Function) => callback(), // call the callback immediately for this test - } as SavedObjectsManagementAction, + setActionContext: () => null, + } as any, ]); const onActionRefresh = jest.fn(); const customizedProps = { ...defaultProps, actionRegistry, onActionRefresh }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index ff3683931d748..4e22d2bd83120 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { IBasePath } from 'src/core/public'; +import { ApplicationStart, IBasePath } from 'src/core/public'; import React, { PureComponent, Fragment } from 'react'; import { EuiSearchBar, @@ -57,7 +57,7 @@ export interface TableProps { onSelectionChange: (selection: SavedObjectWithMetadata[]) => void; }; filterOptions: any[]; - canDelete: boolean; + capabilities: ApplicationStart['capabilities']; onDelete: () => void; onActionRefresh: (object: SavedObjectWithMetadata) => void; onExport: (includeReferencesDeep: boolean) => void; @@ -156,6 +156,7 @@ export class Table extends PureComponent { isSearching, filterOptions, selectionConfig: selection, + capabilities, onDelete, onActionRefresh, selectedSavedObjects, @@ -285,6 +286,7 @@ export class Table extends PureComponent { 'data-test-subj': 'savedObjectsTableAction-relationships', }, ...actionRegistry.getAll().map((action) => { + action.setActionContext({ capabilities }); return { ...action.euiAction, 'data-test-subj': `savedObjectsTableAction-${action.id}`, @@ -354,9 +356,11 @@ export class Table extends PureComponent { iconType="trash" color="danger" onClick={onDelete} - isDisabled={selectedSavedObjects.length === 0 || !this.props.canDelete} + isDisabled={ + selectedSavedObjects.length === 0 || !capabilities.savedObjectsManagement.delete + } title={ - this.props.canDelete + capabilities.savedObjectsManagement.delete ? undefined : i18n.translate('savedObjectsManagement.objectsTable.table.deleteButtonTitle', { defaultMessage: 'Unable to delete saved objects', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index d2db9ab711c57..a5a4bcab364af 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -807,7 +807,7 @@ export class SavedObjectsTable extends Component ReactNode; public abstract id: string; @@ -37,8 +42,13 @@ export abstract class SavedObjectsManagementAction { private callbacks: Function[] = []; + protected actionContext: ActionContext | null = null; protected record: SavedObjectsManagementRecord | null = null; + public setActionContext(actionContext: ActionContext) { + this.actionContext = actionContext; + } + public registerOnFinishCallback(callback: Function) { this.callbacks.push(callback); } diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 2a4b1df743800..2edabbf46f9d8 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -154,6 +154,9 @@ export const VisualizeListing = () => { // we allow users to create visualizations even if they can't save them // for data exploration purposes createItem={createNewVis} + tableCaption={i18n.translate('visualize.listing.table.listTitle', { + defaultMessage: 'Visualizations', + })} findItems={fetchItems} deleteItems={visualizeCapabilities.delete ? deleteItems : undefined} editItem={visualizeCapabilities.save ? editItem : undefined} @@ -161,6 +164,7 @@ export const VisualizeListing = () => { listingLimit={listingLimit} initialPageSize={savedObjectsPublic.settings.getPerPage()} initialFilter={''} + rowHeader="title" noItemsFragment={noItemsFragment} entityName={i18n.translate('visualize.listing.table.entityName', { defaultMessage: 'visualization', diff --git a/test/common/services/kibana_server/kibana_server.ts b/test/common/services/kibana_server/kibana_server.ts index 4a251cca044d3..d808815f7a2c0 100644 --- a/test/common/services/kibana_server/kibana_server.ts +++ b/test/common/services/kibana_server/kibana_server.ts @@ -27,9 +27,13 @@ export function KibanaServerProvider({ getService }: FtrProviderContext) { const config = getService('config'); const lifecycle = getService('lifecycle'); const url = Url.format(config.get('servers.kibana')); - const ssl = config.get('servers.kibana').ssl; const defaults = config.get('uiSettings.defaults'); - const kbn = new KbnClient(log, { url, ssl }, defaults); + const kbn = new KbnClient({ + log, + url, + certificateAuthorities: config.get('servers.kibana.certificateAuthorities'), + uiSettingDefaults: defaults, + }); if (defaults) { lifecycle.beforeTests.add(async () => { diff --git a/test/functional/apps/discover/_date_nanos.js b/test/functional/apps/discover/_date_nanos.js index 1b5c033d67a43..9b076c8215754 100644 --- a/test/functional/apps/discover/_date_nanos.js +++ b/test/functional/apps/discover/_date_nanos.js @@ -27,7 +27,8 @@ export default function ({ getService, getPageObjects }) { const fromTime = 'Sep 22, 2019 @ 20:31:44.000'; const toTime = 'Sep 23, 2019 @ 03:31:44.000'; - describe('date_nanos', function () { + // Failing: See https://github.com/elastic/kibana/issues/82035 + describe.skip('date_nanos', function () { before(async function () { await esArchiver.loadIfNeeded('date_nanos'); await kibanaServer.uiSettings.replace({ defaultIndex: 'date-nanos' }); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index d3c0fe834958d..dceb12a02f87f 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -31,7 +31,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { defaultIndex: 'logstash-*', }; - describe('discover doc table', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/82445 + describe.skip('discover doc table', function describeIndexTests() { const defaultRowsLimit = 50; const rowsHardLimit = 500; diff --git a/test/functional/apps/discover/_sidebar.js b/test/functional/apps/discover/_sidebar.js index f7784b739336b..ce7ebff9cce74 100644 --- a/test/functional/apps/discover/_sidebar.js +++ b/test/functional/apps/discover/_sidebar.js @@ -25,8 +25,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); - // FLAKY: https://github.com/elastic/kibana/issues/80914 - describe.skip('discover sidebar', function describeIndexTests() { + describe('discover sidebar', function describeIndexTests() { before(async function () { // delete .kibana index and update configDoc await kibanaServer.uiSettings.replace({ diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 3e325d5e6b907..972a14842b79e 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -35,7 +35,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'common', ]); - describe('visual builder', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/75127 + describe.skip('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { await security.testUser.setRoles([ diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index 60532c81493f9..2423f66a4b34e 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -349,7 +349,7 @@ export function DiscoverPageProvider({ getService, getPageObjects }: FtrProvider public async closeSidebarFieldFilter() { await testSubjects.click('toggleFieldFilterButton'); - await testSubjects.missingOrFail('filterSelectionPanel', { allowHidden: true }); + await testSubjects.missingOrFail('filterSelectionPanel'); } public async waitForChartLoadingComplete(renderCount: number) { diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index 5bce0d4cf6c87..6e492ad1ced19 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -61,6 +61,8 @@ export async function ScreenshotsProvider({ getService }: FtrProviderContext) { if (updateBaselines) { log.debug('Updating baseline snapshot'); + // Make the directory if it doesn't exist + await mkdirAsync(dirname(baselinePath), { recursive: true }); await writeFileAsync(baselinePath, readFileSync(sessionPath)); return 0; } else { diff --git a/test/plugin_functional/test_suites/data_plugin/session.ts b/test/plugin_functional/test_suites/data_plugin/session.ts index 88241fffae904..93980313838f2 100644 --- a/test/plugin_functional/test_suites/data_plugin/session.ts +++ b/test/plugin_functional/test_suites/data_plugin/session.ts @@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const filterBar = getService('filterBar'); const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); + const esArchiver = getService('esArchiver'); const getSessionIds = async () => { const sessionsBtn = await testSubjects.find('showSessionsButton'); @@ -33,7 +34,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide return sessionIds.split(','); }; - describe('Session management', function describeIndexTests() { + describe('Session management', function describeSessionManagementTests() { describe('Discover', () => { before(async () => { await PageObjects.common.navigateToApp('discover'); @@ -79,5 +80,45 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide expect(sessionIds.length).to.be(1); }); }); + + describe('Dashboard', () => { + before(async () => { + await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data'); + await esArchiver.loadIfNeeded( + '../functional/fixtures/es_archiver/dashboard/current/kibana' + ); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('dashboard with filter'); + await PageObjects.header.waitUntilLoadingHasFinished(); + }); + + afterEach(async () => { + await testSubjects.click('clearSessionsButton'); + await toasts.dismissAllToasts(); + }); + + after(async () => { + await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/data'); + await esArchiver.unload('../functional/fixtures/es_archiver/dashboard/current/kibana'); + }); + + it('on load there is a single session', async () => { + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('starts a session on refresh', async () => { + await testSubjects.click('querySubmitButton'); + await PageObjects.header.waitUntilLoadingHasFinished(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + + it('starts a session on filter change', async () => { + await filterBar.removeAllFilters(); + const sessionIds = await getSessionIds(); + expect(sessionIds.length).to.be(1); + }); + }); }); } diff --git a/test/scripts/server_integration.sh b/test/scripts/server_integration.sh new file mode 100755 index 0000000000000..82bc733e51b26 --- /dev/null +++ b/test/scripts/server_integration.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +source test/scripts/jenkins_test_setup_oss.sh + +yarn run grunt run:serverIntegrationTests diff --git a/test/server_integration/http/ssl/config.js b/test/server_integration/http/ssl/config.js index a7cbd0cce2570..db2451c6ac21c 100644 --- a/test/server_integration/http/ssl/config.js +++ b/test/server_integration/http/ssl/config.js @@ -23,13 +23,14 @@ import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); + const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), supertest: createKibanaSupertestProvider({ - certificateAuthorities: [readFileSync(CA_CERT_PATH)], + certificateAuthorities, }), }, servers: { @@ -37,6 +38,7 @@ export default async function ({ readConfigFile }) { kibana: { ...httpConfig.get('servers.kibana'), protocol: 'https', + certificateAuthorities, }, }, junit: { diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.js index ab3bd2a19c005..392b0434b6284 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.js @@ -17,6 +17,7 @@ * under the License. */ +import Url from 'url'; import { readFileSync } from 'fs'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; @@ -24,22 +25,22 @@ import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); + const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; - const redirectPort = httpConfig.get('servers.kibana.port') + 1; - const supertestOptions = { - ...httpConfig.get('servers.kibana'), - port: redirectPort, - // test with non ssl protocol - protocol: 'http', - }; + const redirectPort = httpConfig.get('servers.kibana.port') + 1234; return { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), supertest: createKibanaSupertestProvider({ - certificateAuthorities: [readFileSync(CA_CERT_PATH)], - options: supertestOptions, + certificateAuthorities, + kibanaUrl: Url.format({ + ...httpConfig.get('servers.kibana'), + port: redirectPort, + // test with non ssl protocol + protocol: 'http', + }), }), }, servers: { @@ -48,6 +49,7 @@ export default async function ({ readConfigFile }) { ...httpConfig.get('servers.kibana'), // start the server with https protocol: 'https', + certificateAuthorities, }, }, junit: { diff --git a/test/server_integration/http/ssl_redirect/index.js b/test/server_integration/http/ssl_redirect/index.js index 3a7b0e310fb23..3893e02696926 100644 --- a/test/server_integration/http/ssl_redirect/index.js +++ b/test/server_integration/http/ssl_redirect/index.js @@ -28,7 +28,7 @@ export default function ({ getService }) { await supertest.get('/').expect('location', url).expect(302); - await supertest.get('/').redirects(1).expect('location', '/app/kibana').expect(302); + await supertest.get('/').redirects(1).expect('location', '/app/home').expect(302); }); }); } diff --git a/test/server_integration/http/ssl_with_p12/config.js b/test/server_integration/http/ssl_with_p12/config.js index 88c03302fb754..906c38538e501 100644 --- a/test/server_integration/http/ssl_with_p12/config.js +++ b/test/server_integration/http/ssl_with_p12/config.js @@ -23,13 +23,14 @@ import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); + const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; return { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), supertest: createKibanaSupertestProvider({ - certificateAuthorities: [readFileSync(CA_CERT_PATH)], + certificateAuthorities, }), }, servers: { @@ -37,6 +38,7 @@ export default async function ({ readConfigFile }) { kibana: { ...httpConfig.get('servers.kibana'), protocol: 'https', + certificateAuthorities, }, }, junit: { diff --git a/test/server_integration/http/ssl_with_p12_intermediate/config.js b/test/server_integration/http/ssl_with_p12_intermediate/config.js index 24f8eefd1077e..5b031bdf8a0fe 100644 --- a/test/server_integration/http/ssl_with_p12_intermediate/config.js +++ b/test/server_integration/http/ssl_with_p12_intermediate/config.js @@ -23,13 +23,14 @@ import { createKibanaSupertestProvider } from '../../services'; export default async function ({ readConfigFile }) { const httpConfig = await readConfigFile(require.resolve('../../config')); + const certificateAuthorities = [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)]; return { testFiles: [require.resolve('./')], services: { ...httpConfig.get('services'), supertest: createKibanaSupertestProvider({ - certificateAuthorities: [readFileSync(CA1_CERT_PATH), readFileSync(CA2_CERT_PATH)], + certificateAuthorities, }), }, servers: { @@ -37,6 +38,7 @@ export default async function ({ readConfigFile }) { kibana: { ...httpConfig.get('servers.kibana'), protocol: 'https', + certificateAuthorities, }, }, junit: { diff --git a/test/server_integration/services/supertest.js b/test/server_integration/services/supertest.js index 9d38b83d4bcd1..f540a0e0755e6 100644 --- a/test/server_integration/services/supertest.js +++ b/test/server_integration/services/supertest.js @@ -21,14 +21,14 @@ import { format as formatUrl } from 'url'; import supertestAsPromised from 'supertest-as-promised'; -export function createKibanaSupertestProvider({ certificateAuthorities, options } = {}) { +export function createKibanaSupertestProvider({ certificateAuthorities, kibanaUrl } = {}) { return function ({ getService }) { const config = getService('config'); - const kibanaServerUrl = options ? formatUrl(options) : formatUrl(config.get('servers.kibana')); + kibanaUrl = kibanaUrl ?? formatUrl(config.get('servers.kibana')); return certificateAuthorities - ? supertestAsPromised.agent(kibanaServerUrl, { ca: certificateAuthorities }) - : supertestAsPromised(kibanaServerUrl); + ? supertestAsPromised.agent(kibanaUrl, { ca: certificateAuthorities }) + : supertestAsPromised(kibanaUrl); }; } diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 91935496b8619..5a8161ebd3608 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -40,7 +40,14 @@ def test() { } def functionalOss(Map params = [:]) { - def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false] + def config = params ?: [ + serverIntegration: true, + ciGroups: true, + firefox: true, + accessibility: true, + pluginFunctional: true, + visualRegression: false, + ] task { kibanaPipeline.buildOss(6) @@ -65,6 +72,10 @@ def functionalOss(Map params = [:]) { if (config.visualRegression) { task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')) } + + if (config.serverIntegration) { + task(kibanaPipeline.scriptTaskDocker('serverIntegration', './test/scripts/server_integration.sh')) + } } } diff --git a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx index 839669bda1098..9c420f4425d04 100644 --- a/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/always_firing.tsx @@ -20,6 +20,7 @@ export function getAlertType(): AlertTypeModel { return { id: 'example.always-firing', name: 'Always Fires', + description: 'Alert when called', iconClass: 'bolt', alertParamsExpression: AlwaysFiringExpression, validate: (alertParams: AlwaysFiringParamsProps['alertParams']) => { diff --git a/x-pack/examples/alerting_example/public/alert_types/astros.tsx b/x-pack/examples/alerting_example/public/alert_types/astros.tsx index 4f894cfe231c9..343f6b10ef85b 100644 --- a/x-pack/examples/alerting_example/public/alert_types/astros.tsx +++ b/x-pack/examples/alerting_example/public/alert_types/astros.tsx @@ -45,6 +45,7 @@ export function getAlertType(): AlertTypeModel { return { id: 'example.people-in-space', name: 'People Are In Space Right Now', + description: 'Alert when people are in space right now', iconClass: 'globe', alertParamsExpression: PeopleinSpaceExpression, validate: (alertParams: PeopleinSpaceParamsProps['alertParams']) => { diff --git a/x-pack/plugins/alerts/common/alert.ts b/x-pack/plugins/alerts/common/alert.ts index 4ebe66f7b7c9f..79e6bb8f2cbba 100644 --- a/x-pack/plugins/alerts/common/alert.ts +++ b/x-pack/plugins/alerts/common/alert.ts @@ -46,6 +46,10 @@ export interface AlertAction { params: AlertActionParams; } +export interface AlertAggregations { + alertExecutionStatus: { [status: string]: number }; +} + export interface Alert { id: string; enabled: boolean; diff --git a/x-pack/plugins/alerts/server/alerts_client.mock.ts b/x-pack/plugins/alerts/server/alerts_client.mock.ts index c9063457fac80..b2ce2a1b356fb 100644 --- a/x-pack/plugins/alerts/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerts/server/alerts_client.mock.ts @@ -11,6 +11,7 @@ export type AlertsClientMock = jest.Mocked; const createAlertsClientMock = () => { const mocked: AlertsClientMock = { + aggregate: jest.fn(), create: jest.fn(), get: jest.fn(), getAlertState: jest.fn(), diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index b35b40b52f7f7..14bddceb1c03d 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -27,6 +27,7 @@ import { SanitizedAlert, AlertTaskState, AlertInstanceSummary, + AlertExecutionStatusValues, } from '../types'; import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib'; import { @@ -98,10 +99,25 @@ export interface FindOptions extends IndexType { filter?: string; } +export interface AggregateOptions extends IndexType { + search?: string; + defaultSearchOperator?: 'AND' | 'OR'; + searchFields?: string[]; + hasReference?: { + type: string; + id: string; + }; + filter?: string; +} + interface IndexType { [key: string]: unknown; } +interface AggregateResult { + alertExecutionStatus: { [status: string]: number }; +} + export interface FindResult { page: number; perPage: number; @@ -400,6 +416,44 @@ export class AlertsClient { }; } + public async aggregate({ + options: { fields, ...options } = {}, + }: { options?: AggregateOptions } = {}): Promise { + // Replace this when saved objects supports aggregations https://github.com/elastic/kibana/pull/64002 + const alertExecutionStatus = await Promise.all( + AlertExecutionStatusValues.map(async (status: string) => { + const { + filter: authorizationFilter, + logSuccessfulAuthorization, + } = await this.authorization.getFindAuthorizationFilter(); + const filter = options.filter + ? `${options.filter} and alert.attributes.executionStatus.status:(${status})` + : `alert.attributes.executionStatus.status:(${status})`; + const { total } = await this.unsecuredSavedObjectsClient.find({ + ...options, + filter: + (authorizationFilter && filter + ? and([esKuery.fromKueryExpression(filter), authorizationFilter]) + : authorizationFilter) ?? filter, + page: 1, + perPage: 0, + type: 'alert', + }); + + logSuccessfulAuthorization(); + + return { [status]: total }; + }) + ); + + return { + alertExecutionStatus: alertExecutionStatus.reduce( + (acc, curr: { [status: string]: number }) => Object.assign(acc, curr), + {} + ), + }; + } + public async delete({ id }: { id: string }) { let taskIdToRemove: string | undefined | null; let apiKeyToInvalidate: string | null = null; diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts new file mode 100644 index 0000000000000..0f89fc6c9c25c --- /dev/null +++ b/x-pack/plugins/alerts/server/alerts_client/tests/aggregate.test.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { AlertsClient, ConstructorOptions } from '../alerts_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; +import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertsAuthorization } from '../../authorization/alerts_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { getBeforeSetup, setGlobalDate } from './lib'; +import { AlertExecutionStatusValues } from '../../types'; + +const taskManager = taskManagerMock.createStart(); +const alertTypeRegistry = alertTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertsAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); + +const kibanaVersion = 'v7.10.0'; +const alertsClientParams: jest.Mocked = { + taskManager, + alertTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertsAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + invalidateAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(alertsClientParams, taskManager, alertTypeRegistry); +}); + +setGlobalDate(); + +describe('aggregate()', () => { + const listedTypes = new Set([ + { + actionGroups: [], + actionVariables: undefined, + defaultActionGroupId: 'default', + id: 'myType', + name: 'myType', + producer: 'myApp', + }, + ]); + beforeEach(() => { + authorization.getFindAuthorizationFilter.mockResolvedValue({ + ensureAlertTypeIsAuthorized() {}, + logSuccessfulAuthorization() {}, + }); + unsecuredSavedObjectsClient.find + .mockResolvedValueOnce({ + total: 10, + per_page: 0, + page: 1, + saved_objects: [], + }) + .mockResolvedValueOnce({ + total: 8, + per_page: 0, + page: 1, + saved_objects: [], + }) + .mockResolvedValueOnce({ + total: 6, + per_page: 0, + page: 1, + saved_objects: [], + }) + .mockResolvedValueOnce({ + total: 4, + per_page: 0, + page: 1, + saved_objects: [], + }) + .mockResolvedValueOnce({ + total: 2, + per_page: 0, + page: 1, + saved_objects: [], + }); + alertTypeRegistry.list.mockReturnValue(listedTypes); + authorization.filterByAlertTypeAuthorization.mockResolvedValue( + new Set([ + { + id: 'myType', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + defaultActionGroupId: 'default', + producer: 'alerts', + authorizedConsumers: { + myApp: { read: true, all: true }, + }, + }, + ]) + ); + }); + + test('calls saved objects client with given params to perform aggregation', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + const result = await alertsClient.aggregate({ options: {} }); + expect(result).toMatchInlineSnapshot(` + Object { + "alertExecutionStatus": Object { + "active": 8, + "error": 6, + "ok": 10, + "pending": 4, + "unknown": 2, + }, + } + `); + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes( + AlertExecutionStatusValues.length + ); + AlertExecutionStatusValues.forEach((status: string, ndx: number) => { + expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([ + { + fields: undefined, + filter: `alert.attributes.executionStatus.status:(${status})`, + page: 1, + perPage: 0, + type: 'alert', + }, + ]); + }); + }); + + test('supports filters when aggregating', async () => { + const alertsClient = new AlertsClient(alertsClientParams); + await alertsClient.aggregate({ options: { filter: 'someTerm' } }); + + expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes( + AlertExecutionStatusValues.length + ); + AlertExecutionStatusValues.forEach((status: string, ndx: number) => { + expect(unsecuredSavedObjectsClient.find.mock.calls[ndx]).toEqual([ + { + fields: undefined, + filter: `someTerm and alert.attributes.executionStatus.status:(${status})`, + page: 1, + perPage: 0, + type: 'alert', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 565b2992b1f7a..75873a2845c15 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -33,6 +33,7 @@ import { } from '../../../../src/core/server'; import { + aggregateAlertRoute, createAlertRoute, deleteAlertRoute, findAlertRoute, @@ -190,6 +191,7 @@ export class AlertingPlugin { // Routes const router = core.http.createRouter(); // Register routes + aggregateAlertRoute(router, this.licenseState); createAlertRoute(router, this.licenseState); deleteAlertRoute(router, this.licenseState); findAlertRoute(router, this.licenseState); diff --git a/x-pack/plugins/alerts/server/routes/aggregate.test.ts b/x-pack/plugins/alerts/server/routes/aggregate.test.ts new file mode 100644 index 0000000000000..498ee7ba2da58 --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/aggregate.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { aggregateAlertRoute } from './aggregate'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { mockLicenseState } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { alertsClientMock } from '../alerts_client.mock'; + +const alertsClient = alertsClientMock.create(); + +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('aggregateAlertRoute', () => { + it('aggregate alerts with proper parameters', async () => { + const licenseState = mockLicenseState(); + const router = httpServiceMock.createRouter(); + + aggregateAlertRoute(router, licenseState); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_aggregate"`); + + const aggregateResult = { + alertExecutionStatus: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }; + alertsClient.aggregate.mockResolvedValueOnce(aggregateResult); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + default_search_operator: 'AND', + }, + }, + ['ok'] + ); + + expect(await handler(context, req, res)).toMatchInlineSnapshot(` + Object { + "body": Object { + "alertExecutionStatus": Object { + "active": 23, + "error": 2, + "ok": 15, + "pending": 1, + "unknown": 0, + }, + }, + } + `); + + expect(alertsClient.aggregate).toHaveBeenCalledTimes(1); + expect(alertsClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "options": Object { + "defaultSearchOperator": "AND", + }, + }, + ] + `); + + expect(res.ok).toHaveBeenCalledWith({ + body: aggregateResult, + }); + }); + + it('ensures the license allows aggregating alerts', async () => { + const licenseState = mockLicenseState(); + const router = httpServiceMock.createRouter(); + + aggregateAlertRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + alertsClient.aggregate.mockResolvedValueOnce({ + alertExecutionStatus: { + ok: 15, + error: 2, + active: 23, + pending: 1, + unknown: 0, + }, + }); + + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + default_search_operator: 'OR', + }, + } + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents aggregating alerts', async () => { + const licenseState = mockLicenseState(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + aggregateAlertRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + const [context, req, res] = mockHandlerArguments( + {}, + { + query: {}, + }, + ['ok'] + ); + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerts/server/routes/aggregate.ts b/x-pack/plugins/alerts/server/routes/aggregate.ts new file mode 100644 index 0000000000000..2c36521b07269 --- /dev/null +++ b/x-pack/plugins/alerts/server/routes/aggregate.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { LicenseState } from '../lib/license_state'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { BASE_ALERT_API_PATH } from '../../common'; +import { renameKeys } from './lib/rename_keys'; +import { FindOptions } from '../alerts_client'; + +// config definition +const querySchema = schema.object({ + search: schema.maybe(schema.string()), + default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], { + defaultValue: 'OR', + }), + search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])), + has_reference: schema.maybe( + // use nullable as maybe is currently broken + // in config-schema + schema.nullable( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ) + ), + filter: schema.maybe(schema.string()), +}); + +export const aggregateAlertRoute = (router: IRouter, licenseState: LicenseState) => { + router.get( + { + path: `${BASE_ALERT_API_PATH}/_aggregate`, + validate: { + query: querySchema, + }, + }, + router.handleLegacyErrors(async function ( + context: RequestHandlerContext, + req: KibanaRequest, unknown>, + res: KibanaResponseFactory + ): Promise { + verifyApiAccess(licenseState); + if (!context.alerting) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); + } + const alertsClient = context.alerting.getAlertsClient(); + + const query = req.query; + const renameMap = { + default_search_operator: 'defaultSearchOperator', + has_reference: 'hasReference', + search: 'search', + filter: 'filter', + }; + + const options = renameKeys>(renameMap, query); + + if (query.search_fields) { + options.searchFields = Array.isArray(query.search_fields) + ? query.search_fields + : [query.search_fields]; + } + + const aggregateResult = await alertsClient.aggregate({ options }); + return res.ok({ + body: aggregateResult, + }); + }) + ); +}; diff --git a/x-pack/plugins/alerts/server/routes/index.ts b/x-pack/plugins/alerts/server/routes/index.ts index aed66e82d11f8..c9c2d03e23601 100644 --- a/x-pack/plugins/alerts/server/routes/index.ts +++ b/x-pack/plugins/alerts/server/routes/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { aggregateAlertRoute } from './aggregate'; export { createAlertRoute } from './create'; export { deleteAlertRoute } from './delete'; export { findAlertRoute } from './find'; diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 7995b1d381700..0eeb31927b2f5 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -17,6 +17,10 @@ export function registerApmAlerts( name: i18n.translate('xpack.apm.alertTypes.errorCount', { defaultMessage: 'Error count threshold', }), + description: i18n.translate('xpack.apm.alertTypes.errorCount.description', { + defaultMessage: + 'Alert when the number of errors in a service exceeds a defined threshold.', + }), iconClass: 'bell', alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), validate: () => ({ @@ -41,6 +45,13 @@ export function registerApmAlerts( name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { defaultMessage: 'Transaction duration threshold', }), + description: i18n.translate( + 'xpack.apm.alertTypes.transactionDuration.description', + { + defaultMessage: + 'Alert when the duration of a specific transaction type in a service exceeds a defined threshold.', + } + ), iconClass: 'bell', alertParamsExpression: lazy( () => import('./TransactionDurationAlertTrigger') @@ -68,6 +79,13 @@ export function registerApmAlerts( name: i18n.translate('xpack.apm.alertTypes.transactionErrorRate', { defaultMessage: 'Transaction error rate threshold', }), + description: i18n.translate( + 'xpack.apm.alertTypes.transactionErrorRate.description', + { + defaultMessage: + 'Alert when the rate of transaction errors in a service exceeds a defined threshold.', + } + ), iconClass: 'bell', alertParamsExpression: lazy( () => import('./TransactionErrorRateAlertTrigger') @@ -95,6 +113,13 @@ export function registerApmAlerts( name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { defaultMessage: 'Transaction duration anomaly', }), + description: i18n.translate( + 'xpack.apm.alertTypes.transactionDurationAnomaly.description', + { + defaultMessage: + 'Alert when the overall transaction duration of a service is considered anomalous.', + } + ), iconClass: 'bell', alertParamsExpression: lazy( () => import('./TransactionDurationAnomalyAlertTrigger') diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 9b3da56b6f640..b9e970ace869d 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -73,8 +73,14 @@ export function registerTransactionDurationAnomalyAlertType({ } const alertParams = params; const request = {} as KibanaRequest; - const { mlAnomalySearch } = ml.mlSystemProvider(request); - const anomalyDetectors = ml.anomalyDetectorsProvider(request); + const { mlAnomalySearch } = ml.mlSystemProvider( + request, + services.savedObjectsClient + ); + const anomalyDetectors = ml.anomalyDetectorsProvider( + request, + services.savedObjectsClient + ); const mlJobs = await getMLJobs(anomalyDetectors, alertParams.environment); @@ -94,6 +100,7 @@ export function registerTransactionDurationAnomalyAlertType({ return {}; } + const jobIds = mlJobs.map((job) => job.job_id); const anomalySearchParams = { terminateAfter: 1, body: { @@ -102,7 +109,7 @@ export function registerTransactionDurationAnomalyAlertType({ bool: { filter: [ { term: { result_type: 'record' } }, - { terms: { job_id: mlJobs.map((job) => job.job_id) } }, + { terms: { job_id: jobIds } }, { range: { timestamp: { @@ -163,7 +170,8 @@ export function registerTransactionDurationAnomalyAlertType({ }; const response = ((await mlAnomalySearch( - anomalySearchParams + anomalySearchParams, + jobIds )) as unknown) as { hits: { total: { value: number } }; aggregations?: { diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index a8a128937fb1c..5e75535c678b3 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -123,8 +123,8 @@ function getMlSetup( request: KibanaRequest ) { return { - mlSystem: ml.mlSystemProvider(request), - anomalyDetectors: ml.anomalyDetectorsProvider(request), + mlSystem: ml.mlSystemProvider(request, savedObjectsClient), + anomalyDetectors: ml.anomalyDetectorsProvider(request, savedObjectsClient), modules: ml.modulesProvider(request, savedObjectsClient), }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 19864cb420b82..5f4bc61af4c69 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -104,7 +104,7 @@ export async function getServiceAnomalies({ }, }; - const response = await ml.mlSystem.mlAnomalySearch(params); + const response = await ml.mlSystem.mlAnomalySearch(params, mlJobIds); return { mlJobIds, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts index 287c7bc2c47f9..8c999b445d799 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/fetcher.ts @@ -81,7 +81,7 @@ export async function anomalySeriesFetcher({ const response: ESSearchResponse< unknown, typeof params - > = (await ml.mlSystem.mlAnomalySearch(params)) as any; + > = (await ml.mlSystem.mlAnomalySearch(params, [jobId])) as any; return response; } catch (err) { diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts index 154821b261fd1..8ab5b33003b86 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/get_ml_bucket_size.ts @@ -49,7 +49,7 @@ export async function getMlBucketSize({ }; try { - const resp = await ml.mlSystem.mlAnomalySearch(params); + const resp = await ml.mlSystem.mlAnomalySearch(params, [jobId]); return resp.hits.hits[0]?._source.bucket_span; } catch (err) { const isHttpError = 'statusCode' in err; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index 22bb556e6d602..bdf555311d154 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -8,6 +8,7 @@ export { mockHistory, mockLocation } from './react_router_history.mock'; export { mockKibanaValues } from './kibana_logic.mock'; export { mockLicensingValues } from './licensing_logic.mock'; export { mockHttpValues } from './http_logic.mock'; +export { mockTelemetryActions } from './telemetry_logic.mock'; export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export { mockAllValues, mockAllActions, setMockValues, setMockActions } from './kea.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index ffbbaaf794bcc..0176f8c03c632 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -13,6 +13,7 @@ import { mockKibanaValues } from './kibana_logic.mock'; import { mockLicensingValues } from './licensing_logic.mock'; import { mockHttpValues } from './http_logic.mock'; +import { mockTelemetryActions } from './telemetry_logic.mock'; import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { @@ -22,6 +23,7 @@ export const mockAllValues = { ...mockFlashMessagesValues, }; export const mockAllActions = { + ...mockTelemetryActions, ...mockFlashMessagesActions, }; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/telemetry_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/telemetry_logic.mock.ts new file mode 100644 index 0000000000000..437e920ef008c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/telemetry_logic.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockTelemetryActions = { + sendTelemetry: jest.fn(), + sendEnterpriseSearchTelemetry: jest.fn(), + sendAppSearchTelemetry: jest.fn(), + sendWorkplaceSearchTelemetry: jest.fn(), +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx index 53f50822cf653..ad60286f4e052 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx @@ -5,17 +5,12 @@ */ import '../../../../__mocks__/kea.mock'; +import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; -jest.mock('../../../../shared/telemetry', () => ({ - sendTelemetry: jest.fn(), - SendAppSearchTelemetry: jest.fn(), -})); -import { sendTelemetry } from '../../../../shared/telemetry'; - import { EmptyState } from './'; describe('EmptyState', () => { @@ -31,7 +26,6 @@ describe('EmptyState', () => { const button = prompt.find(EuiButton); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); - (sendTelemetry as jest.Mock).mockClear(); + expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx index 59986c944c23e..b7ed1cc895097 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx @@ -5,12 +5,11 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendTelemetry } from '../../../../shared/telemetry'; -import { HttpLogic } from '../../../../shared/http'; +import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { CREATE_ENGINES_PATH } from '../../../routes'; @@ -20,15 +19,13 @@ import { EngineOverviewHeader } from './header'; import './empty_state.scss'; export const EmptyState: React.FC = () => { - const { http } = useValues(HttpLogic); + const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const buttonProps = { href: getAppSearchUrl(CREATE_ENGINES_PATH), target: '_blank', onClick: () => - sendTelemetry({ - http, - product: 'app_search', + sendAppSearchTelemetry({ action: 'clicked', metric: 'create_first_engine_button', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx index 78ee5764be5a9..8edb4331c03c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx @@ -6,13 +6,11 @@ import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/enterprise_search_url.mock'; +import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; -jest.mock('../../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../../shared/telemetry'; - import { EngineOverviewHeader } from './'; describe('EngineOverviewHeader', () => { @@ -29,6 +27,6 @@ describe('EngineOverviewHeader', () => { expect(button.prop('isDisabled')).toBeFalsy(); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); + expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx index 6ebb2c5bf453d..4bb69bafa0996 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; import { EuiPageHeader, EuiPageHeaderSection, @@ -16,12 +16,11 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendTelemetry } from '../../../../shared/telemetry'; -import { HttpLogic } from '../../../../shared/http'; +import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; export const EngineOverviewHeader: React.FC = () => { - const { http } = useValues(HttpLogic); + const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const buttonProps = { fill: true, @@ -30,9 +29,7 @@ export const EngineOverviewHeader: React.FC = () => { href: getAppSearchUrl(), target: '_blank', onClick: () => - sendTelemetry({ - http, - product: 'app_search', + sendAppSearchTelemetry({ action: 'clicked', metric: 'header_launch_button', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 4d97a16991b71..37ed45a379c0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -6,14 +6,11 @@ import '../../../__mocks__/kea.mock'; import '../../../__mocks__/enterprise_search_url.mock'; -import { mockHttpValues, mountWithIntl } from '../../../__mocks__/'; +import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__/'; import React from 'react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../shared/telemetry'; - import { EngineTable } from './engine_table'; describe('EngineTable', () => { @@ -58,9 +55,7 @@ describe('EngineTable', () => { expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine'); link.simulate('click'); - expect(sendTelemetry).toHaveBeenCalledWith({ - http: mockHttpValues.http, - product: 'app_search', + expect(mockTelemetryActions.sendAppSearchTelemetry).toHaveBeenCalledWith({ action: 'clicked', metric: 'engine_table_link', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index 40fb313f30b31..ffa5b8e9a1622 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -5,13 +5,12 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { HttpLogic } from '../../../shared/http'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { getEngineRoute } from '../../routes'; @@ -42,15 +41,13 @@ export const EngineTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { - const { http } = useValues(HttpLogic); + const { sendAppSearchTelemetry } = useActions(TelemetryLogic); const engineLinkProps = (name: string) => ({ href: getAppSearchUrl(getEngineRoute(name)), target: '_blank', onClick: () => - sendTelemetry({ - http, - product: 'app_search', + sendAppSearchTelemetry({ action: 'clicked', metric: 'engine_table_link', }), diff --git a/x-pack/plugins/ml/server/lib/license/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts similarity index 81% rename from x-pack/plugins/ml/server/lib/license/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts index 9c4271b65b00d..db74dcb1a1846 100644 --- a/x-pack/plugins/ml/server/lib/license/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MlServerLicense } from './ml_server_license'; +export { Settings } from './settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx new file mode 100644 index 0000000000000..8a51d91180390 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiPageContentBody } from '@elastic/eui'; + +import { Settings } from './settings'; + +describe('Settings', () => { + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiPageContentBody)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx new file mode 100644 index 0000000000000..e5e86f3e39734 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { EuiPageHeader, EuiPageHeaderSection, EuiPageContentBody, EuiTitle } from '@elastic/eui'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; + +export const Settings: React.FC = () => { + return ( + <> + + + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.settings.title', { + defaultMessage: 'Settings', + })} +

+
+
+
+ + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 546ea311ad33e..49e74582f5f15 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -112,9 +112,7 @@ describe('AppSearchNav', () => { setMockValues({ myRole: { canViewSettings: true } }); const wrapper = shallow(); - expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( - 'http://localhost:3002/as/settings/account' - ); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual('/settings/account'); }); it('renders the Credentials link', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index ec5f5b164a7f9..cf67aa3ec7d9d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -32,6 +32,7 @@ import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { NotFound } from '../shared/not_found'; import { EngineOverview } from './components/engine_overview'; +import { Settings } from './components/settings'; import { Credentials } from './components/credentials'; export const AppSearch: React.FC = (props) => { @@ -76,6 +77,9 @@ export const AppSearchConfigured: React.FC = (props) => { + + + @@ -103,9 +107,9 @@ export const AppSearchNav: React.FC = () => { })} {canViewSettings && ( - + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { - defaultMessage: 'Account Settings', + defaultMessage: 'Settings', })} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index b2030ec910cd8..a257ccde9f474 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -5,6 +5,7 @@ */ import '../../../__mocks__/kea.mock'; +import { mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; import { useValues } from 'kea'; @@ -14,11 +15,6 @@ import { EuiCard } from '@elastic/eui'; import { EuiButton } from '../../../shared/react_router_helpers'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -jest.mock('../../../shared/telemetry', () => ({ - sendTelemetry: jest.fn(), -})); -import { sendTelemetry } from '../../../shared/telemetry'; - import { ProductCard } from './'; describe('ProductCard', () => { @@ -38,7 +34,10 @@ describe('ProductCard', () => { expect(button.prop('children')).toEqual('Launch App Search'); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalledWith(expect.objectContaining({ metric: 'app_search' })); + expect(mockTelemetryActions.sendEnterpriseSearchTelemetry).toHaveBeenCalledWith({ + action: 'clicked', + metric: 'app_search', + }); }); it('renders a Workplace Search card', () => { @@ -53,9 +52,10 @@ describe('ProductCard', () => { expect(button.prop('children')).toEqual('Launch Workplace Search'); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalledWith( - expect.objectContaining({ metric: 'workplace_search' }) - ); + expect(mockTelemetryActions.sendEnterpriseSearchTelemetry).toHaveBeenCalledWith({ + action: 'clicked', + metric: 'workplace_search', + }); }); it('renders correct button text when host not present', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 1d05128adc2e3..ee778f49ef5b6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -5,14 +5,13 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; import { snakeCase } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiCard, EuiTextColor } from '@elastic/eui'; import { EuiButton } from '../../../shared/react_router_helpers'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { HttpLogic } from '../../../shared/http'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; @@ -29,7 +28,7 @@ interface IProductCard { } export const ProductCard: React.FC = ({ product, image }) => { - const { http } = useValues(HttpLogic); + const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); const { config } = useValues(KibanaLogic); const LAUNCH_BUTTON_TEXT = i18n.translate( @@ -69,9 +68,7 @@ export const ProductCard: React.FC = ({ product, image }) => { to={product.URL} shouldNotCreateHref={true} onClick={() => - sendTelemetry({ - http, - product: 'enterprise_search', + sendEnterpriseSearchTelemetry({ action: 'clicked', metric: snakeCase(product.ID), }) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts index a8b9636c3ff3e..75a70354aa3d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { sendTelemetry } from './send_telemetry'; +export { TelemetryLogic } from './telemetry_logic'; export { SendEnterpriseSearchTelemetry, SendAppSearchTelemetry, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 5addf9939aad3..46cd09ab74464 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -4,77 +4,50 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../__mocks__/kea.mock'; import '../../__mocks__/shallow_useeffect.mock'; -import { mockHttpValues } from '../../__mocks__'; +import { mockTelemetryActions } from '../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; -import { JSON_HEADER as headers } from '../../../../common/constants'; - import { - sendTelemetry, SendEnterpriseSearchTelemetry, SendAppSearchTelemetry, SendWorkplaceSearchTelemetry, } from './'; -describe('Shared Telemetry Helpers', () => { +describe('Telemetry component helpers', () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('sendTelemetry', () => { - it('successfully calls the server-side telemetry endpoint', () => { - sendTelemetry({ - http: mockHttpValues.http, - product: 'enterprise_search', - action: 'viewed', - metric: 'setup_guide', - }); - - expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { - headers, - body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', - }); - }); - - it('throws an error if the telemetry endpoint fails', () => { - const httpRejectMock = sendTelemetry({ - http: { put: () => Promise.reject() }, - } as any); + it('SendEnterpriseSearchTelemetry', () => { + shallow(); - expect(httpRejectMock).rejects.toThrow('Unable to send telemetry'); + expect(mockTelemetryActions.sendTelemetry).toHaveBeenCalledWith({ + action: 'viewed', + metric: 'page', + product: 'enterprise_search', }); }); - describe('React component helpers', () => { - it('SendEnterpriseSearchTelemetry component', () => { - shallow(); + it('SendAppSearchTelemetry', () => { + shallow(); - expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { - headers, - body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', - }); - }); - - it('SendAppSearchTelemetry component', () => { - shallow(); - - expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { - headers, - body: '{"product":"app_search","action":"clicked","metric":"button"}', - }); + expect(mockTelemetryActions.sendTelemetry).toHaveBeenCalledWith({ + action: 'clicked', + metric: 'button', + product: 'app_search', }); + }); - it('SendWorkplaceSearchTelemetry component', () => { - shallow(); + it('SendWorkplaceSearchTelemetry', () => { + shallow(); - expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { - headers, - body: '{"product":"workplace_search","action":"error","metric":"not_found"}', - }); + expect(mockTelemetryActions.sendTelemetry).toHaveBeenCalledWith({ + action: 'error', + metric: 'not_found', + product: 'workplace_search', }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 2f87597897b41..6d4c06fa16f1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -5,68 +5,40 @@ */ import React, { useEffect } from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; -import { HttpSetup } from 'src/core/public'; -import { JSON_HEADER as headers } from '../../../../common/constants'; -import { HttpLogic } from '../http'; - -interface ISendTelemetryProps { - action: 'viewed' | 'error' | 'clicked'; - metric: string; // e.g., 'setup_guide' -} - -interface ISendTelemetry extends ISendTelemetryProps { - http: HttpSetup; - product: 'app_search' | 'workplace_search' | 'enterprise_search'; -} - -/** - * Base function - useful for non-component actions, e.g. clicks - */ - -export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => { - try { - const body = JSON.stringify({ product, action, metric }); - await http.put('/api/enterprise_search/stats', { headers, body }); - } catch (error) { - throw new Error('Unable to send telemetry'); - } -}; +import { TelemetryLogic, TSendTelemetry } from './telemetry_logic'; /** * React component helpers - useful for on-page-load/views */ -export const SendEnterpriseSearchTelemetry: React.FC = ({ - action, - metric, -}) => { - const { http } = useValues(HttpLogic); +export const SendEnterpriseSearchTelemetry: React.FC = ({ action, metric }) => { + const { sendTelemetry } = useActions(TelemetryLogic); useEffect(() => { - sendTelemetry({ http, action, metric, product: 'enterprise_search' }); - }, [action, metric, http]); + sendTelemetry({ action, metric, product: 'enterprise_search' }); + }, [action, metric]); return null; }; -export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useValues(HttpLogic); +export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { + const { sendTelemetry } = useActions(TelemetryLogic); useEffect(() => { - sendTelemetry({ http, action, metric, product: 'app_search' }); - }, [action, metric, http]); + sendTelemetry({ action, metric, product: 'app_search' }); + }, [action, metric]); return null; }; -export const SendWorkplaceSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useValues(HttpLogic); +export const SendWorkplaceSearchTelemetry: React.FC = ({ action, metric }) => { + const { sendTelemetry } = useActions(TelemetryLogic); useEffect(() => { - sendTelemetry({ http, action, metric, product: 'workplace_search' }); - }, [action, metric, http]); + sendTelemetry({ action, metric, product: 'workplace_search' }); + }, [action, metric]); return null; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts new file mode 100644 index 0000000000000..420d3a5dd4ded --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { resetContext } from 'kea'; + +import { JSON_HEADER as headers } from '../../../../common/constants'; +import { mockHttpValues } from '../../__mocks__'; +jest.mock('../http', () => ({ + HttpLogic: { values: { http: mockHttpValues.http } }, +})); + +import { TelemetryLogic } from './'; + +describe('Telemetry logic', () => { + beforeEach(() => { + jest.clearAllMocks(); + resetContext({}); + TelemetryLogic.mount(); + }); + + describe('sendTelemetry', () => { + it('successfully calls the server-side telemetry endpoint', () => { + TelemetryLogic.actions.sendTelemetry({ + action: 'viewed', + metric: 'setup_guide', + product: 'enterprise_search', + }); + + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { + headers, + body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', + }); + }); + + it('throws an error if the telemetry endpoint fails', async () => { + mockHttpValues.http.put.mockImplementationOnce(() => Promise.reject()); + + // To capture thrown errors, we have to call the listener fn directly + // instead of using `TelemetryLogic.actions.sendTelemetry` - this is + // due to how Kea invokes/wraps action fns by design. + const { sendTelemetry } = (TelemetryLogic.inputs[0] as any).listeners({ actions: {} }); + + await expect(sendTelemetry({ action: '', metric: '', product: '' })).rejects.toThrow( + 'Unable to send telemetry' + ); + }); + }); + + describe('product helpers', () => { + const telemetryEvent = { action: 'viewed', metric: 'overview' }; + + beforeEach(() => { + jest.spyOn(TelemetryLogic.actions, 'sendTelemetry'); + }); + + describe('sendEnterpriseSearchTelemetry', () => { + it('calls sendTelemetry with the product populated', () => { + TelemetryLogic.actions.sendEnterpriseSearchTelemetry(telemetryEvent); + + expect(TelemetryLogic.actions.sendTelemetry).toHaveBeenCalledWith({ + ...telemetryEvent, + product: 'enterprise_search', + }); + }); + }); + + describe('sendAppSearchTelemetry', () => { + it('calls sendTelemetry with the product populated', () => { + TelemetryLogic.actions.sendAppSearchTelemetry(telemetryEvent); + + expect(TelemetryLogic.actions.sendTelemetry).toHaveBeenCalledWith({ + ...telemetryEvent, + product: 'app_search', + }); + }); + }); + + describe('sendWorkplaceSearchTelemetry', () => { + it('calls sendTelemetry with the product populated', () => { + TelemetryLogic.actions.sendWorkplaceSearchTelemetry(telemetryEvent); + + expect(TelemetryLogic.actions.sendTelemetry).toHaveBeenCalledWith({ + ...telemetryEvent, + product: 'workplace_search', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.ts new file mode 100644 index 0000000000000..83fcb2daa795e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { JSON_HEADER as headers } from '../../../../common/constants'; +import { HttpLogic } from '../http'; + +export interface ISendTelemetry { + action: 'viewed' | 'error' | 'clicked'; + metric: string; // e.g., 'setup_guide' + product: 'enterprise_search' | 'app_search' | 'workplace_search'; +} +export type TSendTelemetry = Omit; + +interface ITelemetryActions { + sendTelemetry(args: ISendTelemetry): ISendTelemetry; + sendEnterpriseSearchTelemetry(args: TSendTelemetry): TSendTelemetry; + sendAppSearchTelemetry(args: TSendTelemetry): TSendTelemetry; + sendWorkplaceSearchTelemetry(args: TSendTelemetry): TSendTelemetry; +} + +export const TelemetryLogic = kea>({ + path: ['enterprise_search', 'telemetry_logic'], + actions: { + sendTelemetry: ({ action, metric, product }) => ({ action, metric, product }), + sendEnterpriseSearchTelemetry: ({ action, metric }) => ({ action, metric }), + sendAppSearchTelemetry: ({ action, metric }) => ({ action, metric }), + sendWorkplaceSearchTelemetry: ({ action, metric }) => ({ action, metric }), + }, + listeners: ({ actions }) => ({ + sendTelemetry: async ({ action, metric, product }: ISendTelemetry) => { + const { http } = HttpLogic.values; + try { + const body = JSON.stringify({ product, action, metric }); + await http.put('/api/enterprise_search/stats', { headers, body }); + } catch (error) { + throw new Error('Unable to send telemetry'); + } + }, + sendEnterpriseSearchTelemetry: ({ action, metric }: TSendTelemetry) => + actions.sendTelemetry({ action, metric, product: 'enterprise_search' }), + sendAppSearchTelemetry: ({ action, metric }: TSendTelemetry) => + actions.sendTelemetry({ action, metric, product: 'app_search' }), + sendWorkplaceSearchTelemetry: ({ action, metric }: TSendTelemetry) => + actions.sendTelemetry({ action, metric, product: 'workplace_search' }), + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx new file mode 100644 index 0000000000000..b6b5ba3d1491d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiCopy, EuiButtonIcon, EuiFieldText } from '@elastic/eui'; + +import { CredentialItem } from './'; + +const label = 'Credential'; +const testSubj = 'CredentialItemTest'; +const value = 'foo'; + +const props = { label, testSubj, value }; + +describe('CredentialItem', () => { + const setState = jest.fn(); + const useStateMock: any = (initState: any) => [initState, setState]; + + beforeEach(() => { + jest.spyOn(React, 'useState').mockImplementation(useStateMock); + setState(false); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(`[data-test-subj="${testSubj}"]`)).toHaveLength(1); + }); + + it('renders the copy button', () => { + const copyMock = jest.fn(); + const wrapper = shallow(); + + const copyEl = shallow(
{wrapper.find(EuiCopy).props().children(copyMock)}
); + expect(copyEl.find(EuiButtonIcon).props().onClick).toEqual(copyMock); + }); + + it('does not render copy button when hidden', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiCopy)).toHaveLength(0); + }); + + it('handles credential visible toggle click', () => { + const wrapper = shallow(); + const button = wrapper.find(EuiButtonIcon).dive().find('button'); + button.simulate('click'); + + expect(setState).toHaveBeenCalled(); + expect(wrapper.find(EuiFieldText)).toHaveLength(1); + }); + + it('handles select all button click', () => { + const wrapper = shallow(); + // Toggle isVisible before EuiFieldText is visible + const button = wrapper.find(EuiButtonIcon).dive().find('button'); + button.simulate('click'); + + const simulatedEvent = { + button: 0, + target: { getAttribute: () => '_self' }, + currentTarget: { select: jest.fn() }, + preventDefault: jest.fn(), + }; + + const input = wrapper.find(EuiFieldText).dive().find('input'); + input.simulate('click', simulatedEvent); + + expect(simulatedEvent.currentTarget.select).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.tsx new file mode 100644 index 0000000000000..ce87198f6377a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; + +import { upperFirst } from 'lodash'; + +import { + EuiButtonIcon, + EuiCopy, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiFieldPassword, + EuiToolTip, +} from '@elastic/eui'; + +interface ICredentialItemProps { + label: string; + value: string; + testSubj: string; + hideCopy?: boolean; +} + +const inputSelectAll = (e: React.MouseEvent) => e.currentTarget.select(); + +export const CredentialItem: React.FC = ({ + label, + value, + testSubj, + hideCopy, +}) => { + const [isVisible, setIsVisible] = useState(false); + + return ( + + + + {label} + + + + + {!hideCopy && ( + + + {(copy) => ( + + )} + + + )} + + + setIsVisible(!isVisible)} + iconType={isVisible ? 'eyeClosed' : 'eye'} + color="primary" + /> + + + + {!isVisible ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/index.ts new file mode 100644 index 0000000000000..9bfc3b84de87e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CredentialItem } from './credential_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/index.ts new file mode 100644 index 0000000000000..11549f36df47a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LicenseBadge } from './license_badge'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.scss new file mode 100644 index 0000000000000..f295194b7a130 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.scss @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.wsLicenseBadge { + &__text { + color: white; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx new file mode 100644 index 0000000000000..0b50069a7ece1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiBadge } from '@elastic/eui'; + +import { LicenseBadge } from './'; + +describe('LicenseBadge', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBadge)).toHaveLength(1); + expect(wrapper.find('span').text()).toEqual('Platinum Feature'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx new file mode 100644 index 0000000000000..1e665d1567145 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { EuiBadge } from '@elastic/eui'; + +import './license_badge.scss'; + +const licenseColor = '#00A7B1'; + +export const LicenseBadge: React.FC = () => ( + + Platinum Feature + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index 2013b2609f33b..5e3164de67104 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -5,6 +5,7 @@ */ import '../../../../__mocks__/kea.mock'; +import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -13,12 +14,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ProductButton } from './'; -jest.mock('../../../../shared/telemetry', () => ({ - sendTelemetry: jest.fn(), - SendAppSearchTelemetry: jest.fn(), -})); -import { sendTelemetry } from '../../../../shared/telemetry'; - describe('ProductButton', () => { it('renders', () => { const wrapper = shallow(); @@ -32,7 +27,6 @@ describe('ProductButton', () => { const button = wrapper.find(EuiButton); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); - (sendTelemetry as jest.Mock).mockClear(); + expect(mockTelemetryActions.sendWorkplaceSearchTelemetry).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index 344b442d9a678..278badb303519 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -5,17 +5,16 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { sendTelemetry } from '../../../../shared/telemetry'; -import { HttpLogic } from '../../../../shared/http'; +import { TelemetryLogic } from '../../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; export const ProductButton: React.FC = () => { - const { http } = useValues(HttpLogic); + const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); const buttonProps = { fill: true, @@ -25,9 +24,7 @@ export const ProductButton: React.FC = () => { buttonProps.href = getWorkplaceSearchUrl(); buttonProps.target = '_blank'; buttonProps.onClick = () => - sendTelemetry({ - http, - product: 'workplace_search', + sendWorkplaceSearchTelemetry({ action: 'clicked', metric: 'header_launch_button', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 569e6543ee869..c497e51be96a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -6,6 +6,7 @@ import { IOverviewValues } from '../overview_logic'; +import { setMockValues as setMockKeaValues, setMockActions } from '../../../../__mocks__/kea.mock'; import { DEFAULT_INITIAL_APP_DATA } from '../../../../../../common/__mocks__'; const { workplaceSearch: mockAppValues } = DEFAULT_INITIAL_APP_DATA; @@ -29,14 +30,9 @@ export const mockActions = { const mockValues = { ...mockOverviewValues, ...mockAppValues, isFederatedAuth: true }; -jest.mock('kea', () => ({ - ...(jest.requireActual('kea') as object), - useActions: jest.fn(() => ({ ...mockActions })), - useValues: jest.fn(() => ({ ...mockValues })), -})); - -import { useValues } from 'kea'; +setMockActions({ ...mockActions }); +setMockKeaValues({ ...mockValues }); export const setMockValues = (values: object) => { - (useValues as jest.Mock).mockImplementation(() => ({ ...mockValues, ...values })); + setMockKeaValues({ ...mockValues, ...values }); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index 6be033d7225a8..2c6f43b8cb03b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -6,6 +6,7 @@ import '../../../__mocks__/kea.mock'; import '../../../__mocks__/enterprise_search_url.mock'; +import { mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; @@ -14,9 +15,6 @@ import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { OnboardingCard } from './onboarding_card'; -jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../shared/telemetry'; - const cardProps = { title: 'My card', icon: 'icon', @@ -42,7 +40,7 @@ describe('OnboardingCard', () => { expect(button.prop('href')).toBe('http://localhost:3002/ws/some_path'); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); + expect(mockTelemetryActions.sendWorkplaceSearchTelemetry).toHaveBeenCalled(); }); it('renders an empty button when onboarding is completed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index c1070d57f2856..422e9b1276d4f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { useValues } from 'kea'; +import { useActions } from 'kea'; import { EuiButton, @@ -19,8 +19,7 @@ import { EuiLinkProps, } from '@elastic/eui'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { HttpLogic } from '../../../shared/http'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; interface IOnboardingCardProps { @@ -42,12 +41,10 @@ export const OnboardingCard: React.FC = ({ actionPath, complete, }) => { - const { http } = useValues(HttpLogic); + const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); const onClick = () => - sendTelemetry({ - http, - product: 'workplace_search', + sendWorkplaceSearchTelemetry({ action: 'clicked', metric: 'onboarding_card_button', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 37b3340b96a6a..268e4f8da445a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mockTelemetryActions } from '../../../__mocks__'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; @@ -12,9 +13,6 @@ import { shallow } from 'enzyme'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; -jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../shared/telemetry'; - import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; import { OnboardingCard } from './onboarding_card'; @@ -117,7 +115,7 @@ describe('OnboardingSteps', () => { .find('[data-test-subj="orgNameChangeButton"]'); button.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); + expect(mockTelemetryActions.sendWorkplaceSearchTelemetry).toHaveBeenCalled(); }); it('hides card when name has been changed', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 132824833909d..7251461b848a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; import { EuiSpacer, @@ -22,8 +22,7 @@ import { EuiLinkProps, } from '@elastic/eui'; import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { HttpLogic } from '../../../shared/http'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; @@ -136,12 +135,10 @@ export const OnboardingSteps: React.FC = () => { }; export const OrgNameOnboarding: React.FC = () => { - const { http } = useValues(HttpLogic); + const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); const onClick = () => - sendTelemetry({ - http, - product: 'workplace_search', + sendWorkplaceSearchTelemetry({ action: 'clicked', metric: 'org_name_change_button', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 989ff800483f6..28647a4698a13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mockTelemetryActions } from '../../../__mocks__'; import './__mocks__/overview_logic.mock'; import { setMockValues } from './__mocks__'; @@ -15,9 +16,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { RecentActivity, RecentActivityItem } from './recent_activity'; -jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); -import { sendTelemetry } from '../../../shared/telemetry'; - const organization = { name: 'foo', defaultOrgName: 'bar' }; const activityFeed = [ @@ -50,7 +48,7 @@ describe('RecentActivity', () => { const link = activity.find('[data-test-subj="viewSourceDetailsLink"]'); link.simulate('click'); - expect(sendTelemetry).toHaveBeenCalled(); + expect(mockTelemetryActions.sendWorkplaceSearchTelemetry).toHaveBeenCalled(); }); it('renders activity item error state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index d1b5228123d94..b3bdf7ae2c47d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -7,14 +7,13 @@ import React from 'react'; import moment from 'moment'; -import { useValues } from 'kea'; +import { useValues, useActions } from 'kea'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; -import { sendTelemetry } from '../../../shared/telemetry'; -import { HttpLogic } from '../../../shared/http'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; @@ -94,12 +93,10 @@ export const RecentActivityItem: React.FC = ({ timestamp, sourceId, }) => { - const { http } = useValues(HttpLogic); + const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); const onClick = () => - sendTelemetry({ - http, - product: 'workplace_search', + sendWorkplaceSearchTelemetry({ action: 'clicked', metric: 'recent_activity_source_details_link', }); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 4b1040de52f66..c9ffde49eb00a 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -41,6 +41,7 @@ import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; import { registerEnginesRoute } from './routes/app_search/engines'; import { registerCredentialsRoutes } from './routes/app_search/credentials'; +import { registerSettingsRoutes } from './routes/app_search/settings'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; @@ -128,6 +129,7 @@ export class EnterpriseSearchPlugin implements Plugin { registerConfigDataRoute(dependencies); registerEnginesRoute(dependencies); registerCredentialsRoutes(dependencies); + registerSettingsRoutes(dependencies); registerWSOverviewRoute(dependencies); registerWSGroupRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts new file mode 100644 index 0000000000000..b5f5ad2530a12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSettingsRoutes } from './settings'; + +describe('log settings routes', () => { + describe('GET /api/app_search/log_settings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'get' }); + + registerSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + mockRouter.callRoute({}); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/log_settings', + }); + }); + }); + + describe('PUT /api/app_search/log_settings', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'put', payload: 'body' }); + + registerSettingsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/log_settings', + }); + }); + + describe('validates', () => { + it('validates good data', () => { + const request = { + body: { + analytics: { enabled: true }, + api: { enabled: true }, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('rejects bad data', () => { + const request = { + body: { + foo: 'bar', + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts new file mode 100644 index 0000000000000..b510349839f11 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { IRouteDependencies } from '../../plugin'; + +export function registerSettingsRoutes({ + router, + enterpriseSearchRequestHandler, +}: IRouteDependencies) { + router.get( + { + path: '/api/app_search/log_settings', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/log_settings', + }) + ); + + router.put( + { + path: '/api/app_search/log_settings', + validate: { + body: schema.object({ + api: schema.maybe( + schema.object({ + enabled: schema.boolean(), + }) + ), + analytics: schema.maybe( + schema.object({ + enabled: schema.boolean(), + }) + ), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: '/as/log_settings', + })(context, request, response); + } + ); +} diff --git a/x-pack/plugins/graph/public/components/listing.tsx b/x-pack/plugins/graph/public/components/listing.tsx index b89ee2489d7f3..8423ac1184186 100644 --- a/x-pack/plugins/graph/public/components/listing.tsx +++ b/x-pack/plugins/graph/public/components/listing.tsx @@ -31,7 +31,11 @@ export function Listing(props: ListingProps) { return ( import('./components/expression')), validate: validateMetricThreshold, diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts index 29a58fc95f2be..2e4cb2a53b6b5 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts @@ -15,6 +15,9 @@ export function getAlertType(): AlertTypeModel { name: i18n.translate('xpack.infra.logs.alertFlyout.alertName', { defaultMessage: 'Log threshold', }), + description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { + defaultMessage: 'Alert when the log aggregation exceeds the threshold.', + }), iconClass: 'bell', alertParamsExpression: React.lazy(() => import('./components/expression_editor/editor')), validate: validateExpression, diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index 6a999a86c99d1..a48837792a3cc 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -17,6 +17,9 @@ export function createMetricThresholdAlertType(): AlertTypeModel { name: i18n.translate('xpack.infra.metrics.alertFlyout.alertName', { defaultMessage: 'Metric threshold', }), + description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { + defaultMessage: 'Alert when the metrics aggregation exceeds the threshold.', + }), iconClass: 'bell', alertParamsExpression: React.lazy(() => import('./components/expression')), validate: validateMetricThreshold, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/common.ts b/x-pack/plugins/infra/server/lib/infra_ml/common.ts index 4d2be94c7cd62..ce7e8c690916e 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/common.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/common.ts @@ -62,7 +62,8 @@ export async function getLogEntryDatasets( endTime, COMPOSITE_AGGREGATION_BATCH_SIZE, afterLatestBatchKey - ) + ), + jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index a3a0f91afaab8..c8278bd308758 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -177,7 +177,8 @@ async function fetchMetricsHostsAnomalies( const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination) + createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 1a9b48ade83ed..c8427ef489c4c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -173,7 +173,8 @@ async function fetchMetricK8sAnomalies( const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination) + createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/common.ts b/x-pack/plugins/infra/server/lib/log_analysis/common.ts index 7e4a714a47d1f..e629df16395a4 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/common.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/common.ts @@ -62,7 +62,8 @@ export async function getLogEntryDatasets( endTime, COMPOSITE_AGGREGATION_BATCH_SIZE, afterLatestBatchKey - ) + ), + jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index 0ee123ed2946f..44731fe465d26 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -223,7 +223,8 @@ async function fetchLogEntryAnomalies( const results = decodeOrThrow(logEntryAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createLogEntryAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination, datasets) + createLogEntryAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination, datasets), + jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index 1205c5ae9f61b..cf3abc81e97ce 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -226,7 +226,8 @@ async function fetchTopLogEntryCategories( endTime, categoryCount, datasets - ) + ), + [logEntryCategoriesCountJobId] ) ); @@ -284,7 +285,8 @@ export async function fetchLogEntryCategories( const logEntryCategoriesResponse = decodeOrThrow(logEntryCategoriesResponseRT)( await context.infra.mlSystem.mlAnomalySearch( - createLogEntryCategoriesQuery(logEntryCategoriesCountJobId, categoryIds) + createLogEntryCategoriesQuery(logEntryCategoriesCountJobId, categoryIds), + [logEntryCategoriesCountJobId] ) ); @@ -333,7 +335,8 @@ async function fetchTopLogEntryCategoryHistograms( startTime, endTime, bucketCount - ) + ), + [logEntryCategoriesCountJobId] ) .then(decodeOrThrow(logEntryCategoryHistogramsResponseRT)) .then((response) => ({ diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts index ec5f3c88dff2a..e75d2d017f382 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_datasets_stats.ts @@ -40,7 +40,8 @@ export async function getLatestLogEntriesCategoriesDatasetsStats( endTime, COMPOSITE_AGGREGATION_BATCH_SIZE, afterLatestBatchKey - ) + ), + jobIds ); const { after_key: afterKey, buckets: latestBatchBuckets = [] } = diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts index ce3acd0dba8cf..2e4d9cc1193b3 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_rate_analysis.ts @@ -43,7 +43,8 @@ export async function getLogEntryRateBuckets( COMPOSITE_AGGREGATION_BATCH_SIZE, afterLatestBatchKey, datasets - ) + ), + [logRateJobId] ); const { after_key: afterKey, buckets: latestBatchBuckets = [] } = diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index a83e9ec2a42be..a3b4cb604231f 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -149,8 +149,11 @@ export class InfraServerPlugin { core.http.registerRouteHandlerContext( 'infra', (context, request): InfraRequestHandlerContext => { - const mlSystem = plugins.ml?.mlSystemProvider(request); - const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider(request); + const mlSystem = plugins.ml?.mlSystemProvider(request, context.core.savedObjects.client); + const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider( + request, + context.core.savedObjects.client + ); const spaceId = plugins.spaces?.spacesService.getSpaceId(request) || 'default'; return { diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_policy.ts b/x-pack/plugins/ingest_manager/common/constants/agent_policy.ts index f6261790f6673..ed4b32aeaa50c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_policy.ts @@ -3,15 +3,22 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { AgentPolicyStatus, DefaultPackages } from '../types'; - +import { AgentPolicy, DefaultPackages } from '../types'; export const AGENT_POLICY_SAVED_OBJECT_TYPE = 'ingest-agent-policies'; -export const DEFAULT_AGENT_POLICY = { +export const agentPolicyStatuses = { + Active: 'active', + Inactive: 'inactive', +} as const; + +export const DEFAULT_AGENT_POLICY: Omit< + AgentPolicy, + 'id' | 'updated_at' | 'updated_by' | 'revision' +> = { name: 'Default policy', namespace: 'default', description: 'Default agent policy created by Kibana', - status: AgentPolicyStatus.Active, + status: agentPolicyStatuses.Active, package_policies: [], is_default: true, monitoring_enabled: ['logs', 'metrics'] as Array<'logs' | 'metrics'>, diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index 9b7801cc07202..abd6becae4ae6 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -14,7 +14,16 @@ export const requiredPackages = { Endpoint: 'endpoint', } as const; +export const agentAssetTypes = { + Input: 'input', +} as const; + export const dataTypes = { Logs: 'logs', Metrics: 'metrics', } as const; + +export const installationStatuses = { + Installed: 'installed', + NotInstalled: 'not_installed', +} as const; diff --git a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts index a62fcddd16e0f..8927b5ab3ca4b 100644 --- a/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/package_to_package_policy.test.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PackageInfo, InstallationStatus } from '../types'; +import { installationStatuses } from '../constants'; +import { PackageInfo } from '../types'; import { packageToPackagePolicy, packageToPackagePolicyInputs } from './package_to_package_policy'; describe('Ingest Manager - packageToPackagePolicy', () => { @@ -28,7 +29,7 @@ describe('Ingest Manager - packageToPackagePolicy', () => { map: [], }, }, - status: InstallationStatus.notInstalled, + status: installationStatuses.NotInstalled, }; describe('packageToPackagePolicyInputs', () => { diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts index 0232bd766ca53..f43f65fb317f3 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_policy.ts @@ -3,25 +3,24 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { agentPolicyStatuses } from '../../constants'; +import { DataType, ValueOf } from '../../types'; import { PackagePolicy, PackagePolicyPackage } from './package_policy'; import { Output } from './output'; -export enum AgentPolicyStatus { - Active = 'active', - Inactive = 'inactive', -} +export type AgentPolicyStatus = typeof agentPolicyStatuses; export interface NewAgentPolicy { name: string; namespace: string; description?: string; is_default?: boolean; - monitoring_enabled?: Array<'logs' | 'metrics'>; + monitoring_enabled?: Array>; } export interface AgentPolicy extends NewAgentPolicy { id: string; - status: AgentPolicyStatus; + status: ValueOf; package_policies: string[] | PackagePolicy[]; updated_at: string; updated_by: string; diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 3762fe382e61d..ca4aa3965bc30 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -7,12 +7,16 @@ // Follow pattern from https://github.com/elastic/kibana/pull/52447 // TODO: Update when https://github.com/elastic/kibana/issues/53021 is closed import { SavedObject, SavedObjectAttributes, SavedObjectReference } from 'src/core/public'; -import { dataTypes, requiredPackages } from '../../constants'; +import { + agentAssetTypes, + dataTypes, + installationStatuses, + requiredPackages, +} from '../../constants'; +import { ValueOf } from '../../types'; + +export type InstallationStatus = typeof installationStatuses; -export enum InstallationStatus { - installed = 'installed', - notInstalled = 'not_installed', -} export enum InstallStatus { installed = 'installed', notInstalled = 'not_installed', @@ -27,7 +31,8 @@ export type EpmPackageInstallStatus = 'installed' | 'installing'; export type DetailViewPanelName = 'overview' | 'usages' | 'settings'; export type ServiceName = 'kibana' | 'elasticsearch'; -export type AssetType = KibanaAssetType | ElasticsearchAssetType | AgentAssetType; +export type AgentAssetType = typeof agentAssetTypes; +export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf; export enum KibanaAssetType { dashboard = 'dashboard', @@ -47,10 +52,6 @@ export enum ElasticsearchAssetType { export type DataType = typeof dataTypes; -export enum AgentAssetType { - input = 'input', -} - export type RegistryRelease = 'ga' | 'beta' | 'experimental'; // Fields common to packages that come from direct upload and the registry @@ -235,7 +236,7 @@ interface PackageAdditions { export type PackageList = PackageListItem[]; export type PackageListItem = Installable; -export type PackagesGroupedByStatus = Record; +export type PackagesGroupedByStatus = Record, PackageList>; export type PackageInfo = Installable< // remove the properties we'll be altering/replacing from the base type Omit & @@ -258,12 +259,12 @@ export interface Installation extends SavedObjectAttributes { export type Installable = Installed | NotInstalled; export type Installed = T & { - status: InstallationStatus.installed; + status: InstallationStatus['Installed']; savedObject: SavedObject; }; export type NotInstalled = T & { - status: InstallationStatus.notInstalled; + status: InstallationStatus['NotInstalled']; }; export type AssetReference = KibanaAssetReference | EsAssetReference; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx index 919b6d3669a6b..8a9ba9bae93a9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/components/agent_policy_form.tsx @@ -23,6 +23,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; +import { dataTypes } from '../../../../../../common'; import { NewAgentPolicy, AgentPolicy } from '../../../types'; import { isValidNamespace } from '../../../services'; import { AgentPolicyDeleteProvider } from './agent_policy_delete_provider'; @@ -211,7 +212,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({ = ({ ), }, { - id: 'metrics', + id: dataTypes.Metrics, label: ( <> = ({ { logs: false, metrics: false } )} onChange={(id) => { - if (id !== 'logs' && id !== 'metrics') { + if (id !== dataTypes.Logs && id !== dataTypes.Metrics) { return; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts index d621db615f2bd..9022e312ece79 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/services/validate_package_policy.test..ts @@ -3,12 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - PackageInfo, - InstallationStatus, - NewPackagePolicy, - RegistryPolicyTemplate, -} from '../../../../types'; +import { installationStatuses } from '../../../../../../../common/constants'; +import { PackageInfo, NewPackagePolicy, RegistryPolicyTemplate } from '../../../../types'; import { validatePackagePolicy, validationHasErrors } from './validate_package_policy'; describe('Ingest Manager - validatePackagePolicy()', () => { @@ -31,7 +27,7 @@ describe('Ingest Manager - validatePackagePolicy()', () => { 'index-pattern': [], }, }, - status: InstallationStatus.notInstalled, + status: installationStatuses.NotInstalled, data_streams: [ { dataset: 'foo', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx index d2c3fc64aa9e6..f10f36174fe82 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/components/create_agent_policy.tsx @@ -21,6 +21,7 @@ import { EuiFlyoutProps, EuiSpacer, } from '@elastic/eui'; +import { dataTypes } from '../../../../../../../common'; import { NewAgentPolicy, AgentPolicy } from '../../../../types'; import { useCapabilities, useCore, sendCreateAgentPolicy } from '../../../../hooks'; import { AgentPolicyForm, agentPolicyFormValidation } from '../../components'; @@ -44,7 +45,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({ description: '', namespace: 'default', is_default: undefined, - monitoring_enabled: ['logs', 'metrics'], + monitoring_enabled: Object.values(dataTypes), }); const [isLoading, setIsLoading] = useState(false); const [withSysMonitoring, setWithSysMonitoring] = useState(true); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index 8cf45a13ae8b3..fc1dc44f4b5cc 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; +import { installationStatuses } from '../../../../../../../common/constants'; import { PAGE_ROUTING_PATHS } from '../../../../constants'; import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; @@ -72,7 +73,7 @@ function InstalledPackages() { const allInstalledPackages = allPackages && allPackages.response - ? allPackages.response.filter((pkg) => pkg.status === 'installed') + ? allPackages.response.filter((pkg) => pkg.status === installationStatuses.Installed) : []; const updatablePackages = allInstalledPackages.filter( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx index 1ba1424d21b3a..3f13b65a160d8 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/integration_section.tsx @@ -17,14 +17,14 @@ import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; import { useLink, useGetPackages } from '../../../hooks'; import { Loading } from '../../fleet/components'; -import { InstallationStatus } from '../../../types'; +import { installationStatuses } from '../../../../../../common/constants'; export const OverviewIntegrationSection: React.FC = () => { const { getHref } = useLink(); const packagesRequest = useGetPackages(); const res = packagesRequest.data?.response; const total = res?.length ?? 0; - const installed = res?.filter((p) => p.status === InstallationStatus.installed)?.length ?? 0; + const installed = res?.filter((p) => p.status === installationStatuses.Installed)?.length ?? 0; const updatablePackages = res?.filter( (item) => 'savedObject' in item && item.version > item.savedObject.attributes.version diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index b003d16d379ca..0fd41d074effa 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -18,13 +18,14 @@ import { AgentPolicy, AgentPolicySOAttributes, FullAgentPolicy, - AgentPolicyStatus, ListWithKuery, } from '../types'; import { DeleteAgentPolicyResponse, Settings, + agentPolicyStatuses, storedPackagePoliciesToAgentInputs, + dataTypes, } from '../../common'; import { AgentPolicyNameExistsError } from '../errors'; import { createAgentPolicyAction, listAgents } from './agents'; @@ -61,8 +62,8 @@ class AgentPolicyService { } if ( - oldAgentPolicy.status === AgentPolicyStatus.Inactive && - agentPolicy.status !== AgentPolicyStatus.Active + oldAgentPolicy.status === agentPolicyStatuses.Inactive && + agentPolicy.status !== agentPolicyStatuses.Active ) { throw new Error( `Agent policy ${id} cannot be updated because it is ${oldAgentPolicy.status}` @@ -538,8 +539,8 @@ class AgentPolicyService { monitoring: { use_output: defaultOutput.name, enabled: true, - logs: agentPolicy.monitoring_enabled.indexOf('logs') >= 0, - metrics: agentPolicy.monitoring_enabled.indexOf('metrics') >= 0, + logs: agentPolicy.monitoring_enabled.includes(dataTypes.Logs), + metrics: agentPolicy.monitoring_enabled.includes(dataTypes.Metrics), }, }, } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts index 366c5306a2969..4ca8e9d52c337 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/kibana/index_pattern/install.ts @@ -12,14 +12,9 @@ import { import * as Registry from '../../registry'; import { loadFieldsFromYaml, Fields, Field } from '../../fields/field'; import { getPackageKeysByStatus } from '../../packages/get'; -import { dataTypes } from '../../../../../common/constants'; +import { dataTypes, installationStatuses } from '../../../../../common/constants'; import { ValueOf } from '../../../../../common/types'; -import { - InstallationStatus, - RegistryPackage, - CallESAsCurrentUser, - DataType, -} from '../../../../types'; +import { RegistryPackage, CallESAsCurrentUser, DataType } from '../../../../types'; import { appContextService } from '../../../../services'; interface FieldFormatMap { @@ -87,7 +82,7 @@ export async function installIndexPatterns( // get all user installed packages const installedPackages = await getPackageKeysByStatus( savedObjectsClient, - InstallationStatus.installed + installationStatuses.Installed ); // TODO: move to install package // cache all installed packages if they don't exist diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index cd0dcba7b97b2..2021b353f1a27 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -5,8 +5,9 @@ */ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; -import { isPackageLimited } from '../../../../common'; +import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { ValueOf } from '../../../../common/types'; import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; @@ -58,7 +59,7 @@ export async function getLimitedPackages(options: { const { savedObjectsClient } = options; const allPackages = await getPackages({ savedObjectsClient, experimental: true }); const installedPackages = allPackages.filter( - (pkg) => pkg.status === InstallationStatus.installed + (pkg) => pkg.status === installationStatuses.Installed ); const installedPackagesInfo = await Promise.all( installedPackages.map((pkgInstall) => { @@ -84,12 +85,12 @@ export async function getPackageSavedObjects( export async function getPackageKeysByStatus( savedObjectsClient: SavedObjectsClientContract, - status: InstallationStatus + status: ValueOf ) { const allPackages = await getPackages({ savedObjectsClient, experimental: true }); return allPackages.reduce>((acc, pkg) => { if (pkg.status === status) { - if (pkg.status === InstallationStatus.installed) { + if (pkg.status === installationStatuses.Installed) { // if we're looking for installed packages grab the version from the saved object because `getPackages` will // return the latest package information from the registry acc.push({ pkgName: pkg.name, pkgVersion: pkg.savedObject.attributes.version }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts index 15c45e7449fec..410a9c0b22537 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts @@ -5,14 +5,13 @@ */ import { SavedObject } from 'src/core/server'; -import { RequiredPackage, requiredPackages, ValueOf } from '../../../../common'; import { - AssetType, - Installable, - Installation, - InstallationStatus, - KibanaAssetType, -} from '../../../types'; + RequiredPackage, + requiredPackages, + ValueOf, + installationStatuses, +} from '../../../../common'; +import { AssetType, Installable, Installation, KibanaAssetType } from '../../../types'; export { bulkInstallPackages, isBulkInstallError } from './bulk_install_packages'; export { @@ -56,11 +55,11 @@ export function createInstallableFrom( return savedObject ? { ...from, - status: InstallationStatus.installed, + status: installationStatuses.Installed, savedObject, } : { ...from, - status: InstallationStatus.notInstalled, + status: installationStatuses.NotInstalled, }; } diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent_policy.ts b/x-pack/plugins/ingest_manager/server/types/models/agent_policy.ts index 5fff9247d78d9..a054353e9c9e1 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent_policy.ts @@ -5,14 +5,16 @@ */ import { schema } from '@kbn/config-schema'; import { PackagePolicySchema, NamespaceSchema } from './package_policy'; -import { AgentPolicyStatus } from '../../../common'; +import { agentPolicyStatuses, dataTypes } from '../../../common'; const AgentPolicyBaseSchema = { name: schema.string({ minLength: 1 }), namespace: NamespaceSchema, description: schema.maybe(schema.string()), monitoring_enabled: schema.maybe( - schema.arrayOf(schema.oneOf([schema.literal('logs'), schema.literal('metrics')])) + schema.arrayOf( + schema.oneOf([schema.literal(dataTypes.Logs), schema.literal(dataTypes.Metrics)]) + ) ), }; @@ -24,8 +26,8 @@ export const AgentPolicySchema = schema.object({ ...AgentPolicyBaseSchema, id: schema.string(), status: schema.oneOf([ - schema.literal(AgentPolicyStatus.Active), - schema.literal(AgentPolicyStatus.Inactive), + schema.literal(agentPolicyStatuses.Active), + schema.literal(agentPolicyStatuses.Inactive), ]), package_policies: schema.oneOf([ schema.arrayOf(schema.string()), diff --git a/x-pack/plugins/lens/public/_variables.scss b/x-pack/plugins/lens/public/_variables.scss index 5a4869bb8d84a..1c83a9a0499f1 100644 --- a/x-pack/plugins/lens/public/_variables.scss +++ b/x-pack/plugins/lens/public/_variables.scss @@ -3,3 +3,4 @@ $lnsPanelMinWidth: $euiSize * 18; // These sizes also match canvas' page thumbnails for consistency $lnsSuggestionHeight: 100px; $lnsSuggestionWidth: 150px; +$lnsLayerPanelDimensionMargin: 8px; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index c74ac951907e4..bd43a1dcc20bd 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -151,6 +151,11 @@ export async function mountApp( trackUiEvent('loaded_404'); return ; } + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = params.history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); params.element.classList.add('lnsAppWrapper'); render( @@ -171,5 +176,6 @@ export async function mountApp( return () => { instance.unmount(); unmountComponentAtNode(params.element); + unlistenParentHistory(); }; } diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss index 410aaef9a5195..8766c9f0acabf 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.scss +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.scss @@ -20,11 +20,6 @@ } } -// Draggable item when it is moving -.lnsDragDrop-isHidden { - opacity: 0; -} - // Drop area .lnsDragDrop-isDroppable { @include lnsDroppable; @@ -35,6 +30,10 @@ @include lnsDroppableActive; } +.lnsDragDrop-isActiveGroup { + background-color: transparentize($euiColorVis0, .75); +} + // Drop area while hovering with item .lnsDragDrop-isActiveDropTarget { @include lnsDroppableActiveHover; @@ -52,3 +51,38 @@ text-decoration: line-through; } } + +.lnsDragDrop__reorderableContainer { + position: relative; +} + +.lnsDragDrop__reorderableDrop { + position: absolute; + width: 100%; + top: 0; + height: calc(100% + #{$lnsLayerPanelDimensionMargin}); +} + +.lnsDragDrop-isReorderable { + transition: transform $euiAnimSpeedFast ease-in-out; + pointer-events: none; +} + +// Draggable item when it is moving +.lnsDragDrop-isHidden { + opacity: 0; +} + +.lnsDragDrop__keyboardHandler { + top: 0; + position: absolute; + width: 100%; + height: 100%; + border-radius: $euiBorderRadius; + + &:focus, + &:focus-within { + @include euiFocusRing; + pointer-events: none; + } +} diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index b1cc4c06c2165..8d381dff351c9 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -6,15 +6,16 @@ import React from 'react'; import { render, mount } from 'enzyme'; -import { DragDrop } from './drag_drop'; -import { ChildDragDropProvider } from './providers'; +import { DragDrop, ReorderableDragDrop, DropToHandler, DropHandler } from './drag_drop'; +import { ChildDragDropProvider, ReorderProvider } from './providers'; jest.useFakeTimers(); describe('DragDrop', () => { + const value = { id: '1', label: 'hello' }; test('renders if nothing is being dragged', () => { const component = render( - + ); @@ -54,7 +55,6 @@ describe('DragDrop', () => { setData: jest.fn(), getData: jest.fn(), }; - const value = {}; const component = mount( @@ -77,10 +77,9 @@ describe('DragDrop', () => { const stopPropagation = jest.fn(); const setDragging = jest.fn(); const onDrop = jest.fn(); - const value = {}; const component = mount( - + @@ -94,7 +93,7 @@ describe('DragDrop', () => { expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); expect(setDragging).toBeCalledWith(undefined); - expect(onDrop).toBeCalledWith('hola'); + expect(onDrop).toBeCalledWith({ id: '2', label: 'hi' }); }); test('drop function is not called on droppable=false', async () => { @@ -104,8 +103,8 @@ describe('DragDrop', () => { const onDrop = jest.fn(); const component = mount( - - + + @@ -138,8 +137,8 @@ describe('DragDrop', () => { test('items that have droppable=false get special styling when another item is dragged', () => { const component = mount( - {}}> - + {}}> + {}} droppable={false}> @@ -152,16 +151,16 @@ describe('DragDrop', () => { }); test('additional styles are reflected in the className until drop', () => { - let dragging: string | undefined; + let dragging: { id: '1' } | undefined; const getAdditionalClasses = jest.fn().mockReturnValue('additional'); const component = mount( { - dragging = 'hello'; + dragging = { id: '1' }; }} > - + { component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('drop'); expect(component.find('.additional')).toHaveLength(0); }); + + describe('reordering', () => { + const mountComponent = ( + dragging: { id: '1' } | undefined, + onDrop: DropHandler = jest.fn(), + dropTo: DropToHandler = jest.fn() + ) => + mount( + { + dragging = { id: '1' }; + }} + > + + + 1 + + + 2 + + + 3 + + + + ); + test(`ReorderableDragDrop component doesn't appear for groups of 1 or less`, () => { + let dragging; + const component = mount( + { + dragging = { id: '1' }; + }} + > + + +
+ + + + ); + expect(component.find(ReorderableDragDrop)).toHaveLength(0); + }); + test(`Reorderable component renders properly`, () => { + const component = mountComponent(undefined, jest.fn()); + expect(component.find(ReorderableDragDrop)).toHaveLength(3); + }); + test(`Elements between dragged and drop get extra class to show the reorder effect when dragging`, () => { + const component = mountComponent({ id: '1' }, jest.fn()); + const dataTransfer = { + setData: jest.fn(), + getData: jest.fn(), + }; + component + .find(ReorderableDragDrop) + .first() + .find('[data-test-subj="lnsDragDrop"]') + .simulate('dragstart', { dataTransfer }); + jest.runAllTimers(); + + component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragover'); + expect(component.find('[data-test-subj="lnsDragDrop"]').at(0).prop('style')).toEqual({}); + expect(component.find('[data-test-subj="lnsDragDrop"]').at(1).prop('style')).toEqual({ + transform: 'translateY(-8px)', + }); + expect(component.find('[data-test-subj="lnsDragDrop"]').at(2).prop('style')).toEqual({ + transform: 'translateY(-8px)', + }); + + component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragleave'); + expect(component.find('[data-test-subj="lnsDragDrop"]').at(1).prop('style')).toEqual({}); + expect(component.find('[data-test-subj="lnsDragDrop"]').at(2).prop('style')).toEqual({}); + }); + test(`Dropping an item runs onDrop function`, () => { + const preventDefault = jest.fn(); + const stopPropagation = jest.fn(); + const onDrop = jest.fn(); + + const component = mountComponent({ id: '1' }, onDrop); + + component + .find('[data-test-subj="lnsDragDrop-reorderableDrop"]') + .at(1) + .simulate('drop', { preventDefault, stopPropagation }); + expect(preventDefault).toBeCalled(); + expect(stopPropagation).toBeCalled(); + expect(onDrop).toBeCalledWith({ id: '1' }); + }); + test(`Keyboard navigation: user can reorder an element`, () => { + const onDrop = jest.fn(); + const dropTo = jest.fn(); + const component = mountComponent({ id: '1' }, onDrop, dropTo); + const keyboardHandler = component + .find(ReorderableDragDrop) + .at(1) + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + expect(dropTo).toBeCalledWith('3'); + + keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); + expect(dropTo).toBeCalledWith('1'); + }); + test(`Keyboard Navigation: User cannot move an element outside of the group`, () => { + const onDrop = jest.fn(); + const dropTo = jest.fn(); + const component = mountComponent({ id: '1' }, onDrop, dropTo); + const keyboardHandler = component + .find(ReorderableDragDrop) + .first() + .find('[data-test-subj="lnsDragDrop-keyboardHandler"]'); + + keyboardHandler.simulate('keydown', { key: 'Space' }); + keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); + expect(dropTo).not.toHaveBeenCalled(); + + keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); + expect(dropTo).toBeCalledWith('2'); + }); + }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index b36415fee5b15..c0e01ad93fe83 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -5,19 +5,25 @@ */ import './drag_drop.scss'; - -import React, { useState, useContext } from 'react'; +import React, { useState, useContext, useEffect } from 'react'; import classNames from 'classnames'; -import { DragContext } from './providers'; +import { keys, EuiScreenReaderOnly } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DragContext, DragContextState, ReorderContext, ReorderState } from './providers'; import { trackUiEvent } from '../lens_ui_telemetry'; -type DroppableEvent = React.DragEvent; +export type DroppableEvent = React.DragEvent; /** * A function that handles a drop event. */ export type DropHandler = (item: unknown) => void; +/** + * A function that handles a dropTo event. + */ +export type DropToHandler = (dropTargetId: string) => void; + /** * The base props to the DragDrop component. */ @@ -27,18 +33,23 @@ interface BaseProps { */ className?: string; + /** + * The event handler that fires when this item + * is dropped to the one with passed id + * + */ + dropTo?: DropToHandler; /** * The event handler that fires when an item * is dropped onto this DragDrop component. */ onDrop?: DropHandler; - /** * The value associated with this item, if it is draggable. * If this component is dragged, this will be the value of * "dragging" in the root drag/drop context. */ - value?: unknown; + value?: DragContextState['dragging']; /** * Optional comparison function to check whether a value is the dragged one @@ -66,17 +77,22 @@ interface BaseProps { */ 'data-test-subj'?: string; + /** + * items belonging to the same group that can be reordered + */ + itemsInGroup?: string[]; + /** * Indicates to the user whether the currently dragged item * will be moved or copied */ - dragType?: 'copy' | 'move'; + dragType?: 'copy' | 'move' | 'reorder'; /** * Indicates to the user whether the drop action will * replace something that is existing or add a new one */ - dropType?: 'add' | 'replace'; + dropType?: 'add' | 'replace' | 'reorder'; } /** @@ -116,10 +132,12 @@ type Props = DraggableProps | NonDraggableProps; export const DragDrop = (props: Props) => { const { dragging, setDragging } = useContext(DragContext); const { value, draggable, droppable, isValueEqual } = props; + return ( { // draggable and drop targets droppable === false && Boolean(dragging) && value !== dragging } - setDragging={setDragging} /> ); }; const DragDropInner = React.memo(function DragDropInner( - props: Props & { - dragging: unknown; - setDragging: (dragging: unknown) => void; - isDragging: boolean; - isNotDroppable: boolean; - } + props: Props & + DragContextState & { + isDragging: boolean; + isNotDroppable: boolean; + } ) { const [state, setState] = useState({ isActive: false, @@ -159,6 +175,8 @@ const DragDropInner = React.memo(function DragDropInner( isNotDroppable, dragType = 'copy', dropType = 'add', + dropTo, + itemsInGroup, } = props; const isMoveDragging = isDragging && dragType === 'move'; @@ -170,12 +188,11 @@ const DragDropInner = React.memo(function DragDropInner( 'lnsDragDrop-isDragging': isDragging, 'lnsDragDrop-isHidden': isMoveDragging, 'lnsDragDrop-isDroppable': !draggable, - 'lnsDragDrop-isDropTarget': droppable, - 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive, + 'lnsDragDrop-isDropTarget': droppable && dragType !== 'reorder', + 'lnsDragDrop-isActiveDropTarget': droppable && state.isActive && dragType !== 'reorder', 'lnsDragDrop-isNotDroppable': !isMoveDragging && isNotDroppable, 'lnsDragDrop-isReplacing': droppable && state.isActive && dropType === 'replace', }, - className, state.dragEnterClassNames ); @@ -193,6 +210,7 @@ const DragDropInner = React.memo(function DragDropInner( // Chrome causes issues if you try to render from within a // dragStart event, so we drop a setTimeout to avoid that. + setState({ ...state }); setTimeout(() => setDragging(value)); }; @@ -237,9 +255,48 @@ const DragDropInner = React.memo(function DragDropInner( } }; + const isReorderDragging = !!(dragging && itemsInGroup?.includes(dragging.id)); + + if ( + draggable && + itemsInGroup?.length && + itemsInGroup.length > 1 && + value?.id && + dropTo && + (!dragging || isReorderDragging) + ) { + const { label } = props as DraggableProps; + return ( + + {children} + + ); + } return React.cloneElement(children, { 'data-test-subj': props['data-test-subj'] || 'lnsDragDrop', - className: classNames(children.props.className, classes), + className: classNames(children.props.className, classes, className), onDragOver: dragOver, onDragLeave: dragLeave, onDrop: drop, @@ -248,3 +305,222 @@ const DragDropInner = React.memo(function DragDropInner( onDragStart: dragStart, }); }); + +const getKeyboardReorderMessageMoved = ( + itemLabel: string, + position: number, + prevPosition: number +) => + i18n.translate('xpack.lens.dragDrop.elementMoved', { + defaultMessage: `You have moved the item {itemLabel} from position {prevPosition} to position {position}`, + values: { + itemLabel, + position, + prevPosition, + }, + }); + +const getKeyboardReorderMessageLifted = (itemLabel: string, position: number) => + i18n.translate('xpack.lens.dragDrop.elementLifted', { + defaultMessage: `You have lifted an item {itemLabel} in position {position}`, + values: { + itemLabel, + position, + }, + }); + +const lnsLayerPanelDimensionMargin = 8; + +export const ReorderableDragDrop = ({ + draggingProps, + dropProps, + children, + label, + dropTo, + className, +}: { + draggingProps: { + className: string; + draggable: Props['draggable']; + onDragEnd: (e: DroppableEvent) => void; + onDragStart: (e: DroppableEvent) => void; + dataTestSubj: string; + isReorderDragging: boolean; + }; + dropProps: { + onDrop: (e: DroppableEvent) => void; + onDragOver: (e: DroppableEvent) => void; + onDragLeave: () => void; + dragging: DragContextState['dragging']; + droppable: DraggableProps['droppable']; + itemsInGroup: string[]; + id: string; + isActive: boolean; + }; + children: React.ReactElement; + label: string; + dropTo: DropToHandler; + className?: string; +}) => { + const { itemsInGroup, dragging, id, droppable } = dropProps; + const { reorderState, setReorderState } = useContext(ReorderContext); + + const { isReorderOn, reorderedItems, draggingHeight, direction, groupId } = reorderState; + const currentIndex = itemsInGroup.indexOf(id); + + useEffect( + () => + setReorderState((s: ReorderState) => ({ + ...s, + isReorderOn: draggingProps.isReorderDragging, + })), + [draggingProps.isReorderDragging, setReorderState] + ); + + return ( +
+ +