diff --git a/.eslintrc.js b/.eslintrc.js index f1e0b7d9353e8..529bc68537aa1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -133,7 +133,7 @@ module.exports = { * Licence headers */ { - files: ['**/*.{js,ts,tsx}'], + files: ['**/*.{js,ts,tsx}', '!plugins/**/*'], rules: { '@kbn/eslint/require-license-header': [ 'error', @@ -219,6 +219,8 @@ module.exports = { // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 '!src/core/server/*.test.mocks{,.ts}', + + 'target/types/**', ], allowSameFolder: true, errorMessage: diff --git a/docs/apm/images/apm-agent-configuration.png b/docs/apm/images/apm-agent-configuration.png index ded0553219a03..05518cb924d1b 100644 Binary files a/docs/apm/images/apm-agent-configuration.png and b/docs/apm/images/apm-agent-configuration.png differ diff --git a/docs/apm/images/apm-alert.png b/docs/apm/images/apm-alert.png index 4cee7214637f8..43c6faa41c75e 100644 Binary files a/docs/apm/images/apm-alert.png and b/docs/apm/images/apm-alert.png differ diff --git a/docs/apm/images/apm-errors-overview.png b/docs/apm/images/apm-errors-overview.png index 905487d2802bc..90f16b81e9f50 100644 Binary files a/docs/apm/images/apm-errors-overview.png and b/docs/apm/images/apm-errors-overview.png differ diff --git a/docs/apm/images/apm-query-bar.png b/docs/apm/images/apm-query-bar.png index 57bac78ea1281..313ee7d4b8fc8 100644 Binary files a/docs/apm/images/apm-query-bar.png and b/docs/apm/images/apm-query-bar.png differ diff --git a/docs/apm/images/apm-services-overview.png b/docs/apm/images/apm-services-overview.png index 19068ce8f69db..48236522ddfbb 100644 Binary files a/docs/apm/images/apm-services-overview.png and b/docs/apm/images/apm-services-overview.png differ diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png new file mode 100644 index 0000000000000..4eaef9ec15ac5 Binary files /dev/null and b/docs/apm/images/apm-settings.png differ diff --git a/docs/apm/images/apm-setup.png b/docs/apm/images/apm-setup.png index feff3d47b62e2..3f5f7761427de 100644 Binary files a/docs/apm/images/apm-setup.png and b/docs/apm/images/apm-setup.png differ diff --git a/docs/apm/images/apm-traces.png b/docs/apm/images/apm-traces.png index ba3bbff482af3..6219be5b6d6e4 100644 Binary files a/docs/apm/images/apm-traces.png and b/docs/apm/images/apm-traces.png differ diff --git a/docs/apm/images/apm-transaction-response-dist.png b/docs/apm/images/apm-transaction-response-dist.png index 2309ec2435c81..ecf5a4af2c25d 100644 Binary files a/docs/apm/images/apm-transaction-response-dist.png and b/docs/apm/images/apm-transaction-response-dist.png differ diff --git a/docs/apm/images/apm-transactions-overview.png b/docs/apm/images/apm-transactions-overview.png index c3c10fcb35ea8..b3b6ca22c4f63 100644 Binary files a/docs/apm/images/apm-transactions-overview.png and b/docs/apm/images/apm-transactions-overview.png differ diff --git a/docs/apm/images/jvm-metrics-overview.png b/docs/apm/images/jvm-metrics-overview.png new file mode 100644 index 0000000000000..9c8ba4a12a262 Binary files /dev/null and b/docs/apm/images/jvm-metrics-overview.png differ diff --git a/docs/apm/images/jvm-metrics.png b/docs/apm/images/jvm-metrics.png index 0ca2147ae0e43..1720e1370ff90 100644 Binary files a/docs/apm/images/jvm-metrics.png and b/docs/apm/images/jvm-metrics.png differ diff --git a/docs/apm/metrics.asciidoc b/docs/apm/metrics.asciidoc index e82a4fbd5c291..e64cbc846960d 100644 --- a/docs/apm/metrics.asciidoc +++ b/docs/apm/metrics.asciidoc @@ -11,8 +11,12 @@ For example, you might be able to correlate a high number of errors with a long [role="screenshot"] image::apm/images/apm-metrics.png[Example view of the Metrics overview in APM app in Kibana] -If you're using the Java Agent, the metrics view focuses on JVMs. -A detailed view of metrics per JVM makes it much easier to analyze the provided metrics: +If you're using the Java Agent, you can view metrics for each JVM. + +[role="screenshot"] +image::apm/images/jvm-metrics-overview.png[Example view of the Metrics overview for the Java Agent] + +Breaking down metrics by JVM makes it much easier to analyze the provided metrics: CPU usage, memory usage, heap or non-heap memory, thread count, garbage collection rate, and garbage collection time spent per minute. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 7c50dbf542d0d..da109331ae0fb 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -126,16 +126,22 @@ control the capturing process. [cols="2*<"] |=== | `xpack.reporting.capture.timeouts.openUrl` - | How long to allow the Reporting browser to wait for the initial data of the - {kib} page to load. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the Kibana page. If the time is + exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. + Defaults to `30000` (30 seconds). | `xpack.reporting.capture.timeouts.waitForElements` - | How long to allow the Reporting browser to wait for the visualization panels to - load on the {kib} page. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for all visualization + panels to load on the Kibana page. If the time is exceeded, a page screenshot + is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 + seconds). | `xpack.reporting.capture.timeouts.renderComplete` - | How long to allow the Reporting browser to wait for each visualization to - signal that it is done renderings. Defaults to `30000` (30 seconds). + | Specify how long to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a + page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to + `30000` (30 seconds). |=== diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index cc662af08b8f1..6596f93a88f51 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -225,11 +225,11 @@ If you configure a custom index, the name must be lowercase, and conform to the {es} {ref}/indices-create-index.html[index name limitations]. *Default: `".kibana"`* -| `kibana.autocompleteTimeout:` +| `kibana.autocompleteTimeout:` {ess-icon} | Time in milliseconds to wait for autocomplete suggestions from {es}. This value must be a whole number greater than zero. *Default: `"1000"`* -| `kibana.autocompleteTerminateAfter:` +| `kibana.autocompleteTerminateAfter:` {ess-icon} | Maximum number of documents loaded by each shard to generate autocomplete suggestions. This value must be a whole number greater than zero. *Default: `"100000"`* @@ -300,11 +300,11 @@ suppress all logging output. *Default: `false`* (for example, `America/Los_Angeles`) to log events using that timezone. For a list of timezones, refer to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. *Default: `UTC`* -| [[logging-verbose]] `logging.verbose:` +| [[logging-verbose]] `logging.verbose:` {ece-icon} | Set to `true` to log all events, including system usage information and all -requests. Supported on {ece}. *Default: `false`* +requests. *Default: `false`* -| `map.includeElasticMapsService:` +| `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` and the tile layer configured by `map.tilemap.url` are available in <>. *Default: `true`* @@ -313,9 +313,9 @@ and the tile layer configured by `map.tilemap.url` are available in <> Elastic Maps Service requests through the {kib} server. *Default: `false`* -| [[regionmap-settings]] `map.regionmap:` +| [[regionmap-settings]] `map.regionmap:` {ess-icon} {ece-icon} | Specifies additional vector layers for -use in <> visualizations. Supported on {ece}. Each layer +use in <> visualizations. Each layer object points to an external vector file that contains a geojson FeatureCollection. The file must use the https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] @@ -343,20 +343,19 @@ map.regionmap: [cols="2*<"] |=== -| [[regionmap-ES-map]] `map.includeElasticMapsService:` +| [[regionmap-ES-map]] `map.includeElasticMapsService:` {ece-icon} | Turns on or off whether layers from the Elastic Maps Service should be included in the vector -layer option list. Supported on {ece}. By turning this off, +layer option list. By turning this off, only the layers that are configured here will be included. The default is `true`. This also affects whether tile-service from the Elastic Maps Service will be available. -| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` +| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` {ess-icon} {ece-icon} | Optional. References the originating source of the geojson file. -Supported on {ece}. -| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` +| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` {ess-icon} {ece-icon} | Mandatory. Each layer can contain multiple fields to indicate what properties from the geojson -features you wish to expose. Supported on {ece}. The following shows how to define multiple +features you wish to expose. The following shows how to define multiple properties: |=== @@ -379,44 +378,44 @@ map.regionmap: [cols="2*<"] |=== -| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` +| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` {ess-icon} {ece-icon} | Mandatory. The human readable text that is shown under the Options tab when -building the Region Map visualization. Supported on {ece}. +building the Region Map visualization. -| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` +| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` {ess-icon} {ece-icon} | Mandatory. This value is used to do an inner-join between the document stored in {es} and the geojson file. For example, if the field in the geojson is called `Location` and has city names, there must be a field in {es} that holds the same values that {kib} can then use to lookup for the geoshape -data. Supported on {ece}. +data. -| [[regionmap-name]] `map.regionmap.layers[].name:` +| [[regionmap-name]] `map.regionmap.layers[].name:` {ess-icon} {ece-icon} | Mandatory. A description of -the map being provided. Supported on {ece}. +the map being provided. -| [[regionmap-url]] `map.regionmap.layers[].url:` +| [[regionmap-url]] `map.regionmap.layers[].url:` {ess-icon} {ece-icon} | Mandatory. The location of the -geojson file as provided by a webserver. Supported on {ece}. +geojson file as provided by a webserver. -| [[tilemap-settings]] `map.tilemap.options.attribution:` - | The map attribution string. Supported on {ece}. +| [[tilemap-settings]] `map.tilemap.options.attribution:` {ess-icon} {ece-icon} + | The map attribution string. *Default: `"© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)"`* -| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` - | The maximum zoom level. Supported on {ece}. *Default: `10`* +| [[tilemap-max-zoom]] `map.tilemap.options.maxZoom:` {ess-icon} {ece-icon} + | The maximum zoom level. *Default: `10`* -| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` - | The minimum zoom level. Supported on {ece}. *Default: `1`* +| [[tilemap-min-zoom]] `map.tilemap.options.minZoom:` {ess-icon} {ece-icon} + | The minimum zoom level. *Default: `1`* -| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` +| [[tilemap-subdomains]] `map.tilemap.options.subdomains:` {ess-icon} {ece-icon} | An array of subdomains used by the tile service. Specify the position of the subdomain the URL with -the token `{s}`. Supported on {ece}. +the token `{s}`. -| [[tilemap-url]] `map.tilemap.url:` +| [[tilemap-url]] `map.tilemap.url:` {ess-icon} {ece-icon} | The URL to the tileservice that {kib} uses -to display map tiles in tilemap visualizations. Supported on {ece}. By default, +to display map tiles in tilemap visualizations. By default, {kib} reads this URL from an external metadata service, but users can override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` @@ -451,7 +450,7 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. This setting may not be used when `server.compression.enabled` is set to `false`. *Default: `none`* -| `server.customResponseHeaders:` +| `server.customResponseHeaders:` {ess-icon} | Header names and values to send on all responses to the client from the {kib} server. *Default: `{}`* @@ -610,7 +609,7 @@ us improve your user experience. Your data is never shared with anyone. Set to `false` to disable telemetry capabilities entirely. You can alternatively opt out through *Advanced Settings*. *Default: `true`* -| `vis_type_vega.enableExternalUrls:` +| `vis_type_vega.enableExternalUrls:` {ess-icon} | Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {es}. *Default: `false`* @@ -622,7 +621,7 @@ disable the License Management UI. *Default: `true`* | Set this value to false to disable the Rollup UI. *Default: true* -| `i18n.locale` +| `i18n.locale` {ess-icon} | Set this value to change the {kib} interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* diff --git a/docs/user/getting-started.asciidoc b/docs/user/getting-started.asciidoc index d426ec111351c..0eb823dcc720f 100644 --- a/docs/user/getting-started.asciidoc +++ b/docs/user/getting-started.asciidoc @@ -8,18 +8,9 @@ Ready to try out {kib} and see what it can do? To quickest way to get started wi [float] [[cloud-set-up]] -== Set up on Cloud +== Set up on cloud -To access {kib} in a single click, run our hosted Elasticsearch Service on Elastic Cloud. - -. Log into the link:https://cloud.elastic.co/[Elasticsearch Service Console]. -If you need an account, register for a link:https://www.elastic.co/cloud/elasticsearch-service/signup[free 14-day trial]. - -. Click *Create deployment*, then give your deployment a name. - -. To use the default options, click *Create deployment*. You can modify the other deployment options, but the default options are great to get started. - -Be sure to copy down the password for the `elastic` user and Cloud ID information. You'll need that later. +include::{docs-root}/shared/cloud/ess-getting-started.asciidoc[] [float] [[get-data-in]] diff --git a/docs/user/security/api-keys/index.asciidoc b/docs/user/security/api-keys/index.asciidoc index c00f58cf598e3..b8d6649a3fb85 100644 --- a/docs/user/security/api-keys/index.asciidoc +++ b/docs/user/security/api-keys/index.asciidoc @@ -3,18 +3,18 @@ === API Keys -API keys enable you to create secondary credentials so that you can send -requests on behalf of the user. Secondary credentials have -the same or lower access rights. +API keys enable you to create secondary credentials so that you can send +requests on behalf of the user. Secondary credentials have +the same or lower access rights. For example, if you extract data from an {es} cluster on a daily -basis, you might create an API key tied to your credentials, -configure it with minimum access, +basis, you might create an API key tied to your credentials, +configure it with minimum access, and then put the API credentials into a cron job. -Or, you might create API keys to automate ingestion of new data from -remote sources, without a live user interaction. +Or, you might create API keys to automate ingestion of new data from +remote sources, without a live user interaction. -You can create API keys from the {kib} Console. To view and invalidate +You can create API keys from the {kib} Console. To view and invalidate API keys, use *Management > Security > API Keys*. [role="screenshot"] @@ -24,63 +24,80 @@ image:user/security/api-keys/images/api-keys.png["API Keys UI"] [[api-keys-service]] === {es} API key service -The {es} API key service is automatically enabled when you configure -{ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. +The {es} API key service is automatically enabled when you configure +{ref}/configuring-tls.html#tls-http[TLS on the HTTP interface]. This ensures that clients are unable to send API keys in clear-text. -When HTTPS connections are not enabled between {kib} and {es}, +When HTTPS connections are not enabled between {kib} and {es}, you cannot create or manage API keys, and you get an error message. -For more information, see the -{ref}/security-api-create-api-key.html[{es} API key documentation], +For more information, see the +{ref}/security-api-create-api-key.html[{es} API key documentation], or contact your system administrator. [float] [[api-keys-security-privileges]] === Security privileges -You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key` -cluster privileges to use API keys in {kib}. You can manage roles in -*Management > Security > Roles*, or use the <>. +You must have the `manage_security`, `manage_api_key`, or the `manage_own_api_key` +cluster privileges to use API keys in {kib}. You can manage roles in +*Management > Security > Roles*, or use the <>. [float] [[create-api-key]] === Create an API key -You can {ref}/security-api-create-api-key.html[create an API key] from -the Kibana Console. For example: +You can {ref}/security-api-create-api-key.html[create an API key] from +the {kib} Console. This example shows how to create an API key +to authenticate to a <>. [source,js] POST /_security/api_key { - "name": "my_api_key", - "expiration": "1d" + "name": "kibana_api_key", } -This creates an API key with the name `my_api_key` that -expires after one day. API key names must be globally unique. -An expiration date is optional and follows {ref}/common-options.html#time-units[{es} time unit format]. +This creates an API key with the +name `kibana_api_key`. API key +names must be globally unique. +An expiration date is optional and follows +{ref}/common-options.html#time-units[{es} time unit format]. When an expiration is not provided, the API key does not expire. +The response should look something like this: + +[source,js] +{ + "id" : "XFcbCnIBnbwqt2o79G4q", + "name" : "kibana_api_key", + "api_key" : "FD6P5UA4QCWlZZQhYF3YGw" +} + +Now, you can use the API key to request {kib} roles. You will need +to base64-encode the `id` and `api_key` provided in the response +and add it to your request as an authorization header. For example: + +[source,js] +curl --location --request GET 'http://localhost:5601/api/security/role' \ +--header 'Content-Type: application/json;charset=UTF-8' \ +--header 'kbn-xsrf: true' \ +--header 'Authorization: ApiKey aVZlLUMzSUJuYndxdDJvN0k1bU46aGxlYUpNS2lTa2FKeVZua1FnY1VEdw==' \ + [float] [[view-api-keys]] === View and invalidate API keys -The *API Keys* UI lists your API keys, including the name, date created, +The *API Keys* feature in Kibana lists your API keys, including the name, date created, and expiration date. If an API key expires, its status changes from `Active` to `Expired`. -If you have `manage_security` or `manage_api_key` permissions, -you can view the API keys of all users, and see which API key was +If you have `manage_security` or `manage_api_key` permissions, +you can view the API keys of all users, and see which API key was created by which user in which realm. If you have only the `manage_own_api_key` permission, you see only a list of your own keys. -You can invalidate API keys individually or in bulk. +You can invalidate API keys individually or in bulk. Invalidated keys are deleted in batch after seven days. [role="screenshot"] image:user/security/api-keys/images/api-key-invalidate.png["API Keys invalidate"] -You cannot modify an API key. If you need additional privileges, +You cannot modify an API key. If you need additional privileges, you must create a new key with the desired configuration and invalidate the old key. - - - - diff --git a/package.json b/package.json index 810d9ddb7e337..91034fea5156a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "kbn:watch": "node scripts/kibana --dev --logging.json=false", "build:types": "tsc --p tsconfig.types.json", "docs:acceptApiChanges": "node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept", - "kbn:bootstrap": "yarn build:types && node scripts/register_git_hook", + "kbn:bootstrap": "node scripts/register_git_hook", "spec_to_console": "node scripts/spec_to_console", "backport-skip-ci": "backport --prDescription \"[skip-ci]\"", "storybook": "node scripts/storybook", diff --git a/packages/kbn-es/src/utils/native_realm.js b/packages/kbn-es/src/utils/native_realm.js index 086898abb6b67..f3f5f7bbdf431 100644 --- a/packages/kbn-es/src/utils/native_realm.js +++ b/packages/kbn-es/src/utils/native_realm.js @@ -76,10 +76,6 @@ exports.NativeRealm = class NativeRealm { } const reservedUsers = await this.getReservedUsers(); - if (!reservedUsers || reservedUsers.length < 1) { - throw new Error('no reserved users found, unable to set native realm passwords'); - } - await Promise.all( reservedUsers.map(async user => { await this.setPassword(user, options[`password.${user}`]); @@ -88,16 +84,18 @@ exports.NativeRealm = class NativeRealm { } async getReservedUsers() { - const users = await this._autoRetry(async () => { - return await this._client.security.getUser(); - }); + return await this._autoRetry(async () => { + const resp = await this._client.security.getUser(); + const usernames = Object.keys(resp.body).filter( + user => resp.body[user].metadata._reserved === true + ); - return Object.keys(users.body).reduce((acc, user) => { - if (users.body[user].metadata._reserved === true) { - acc.push(user); + if (!usernames?.length) { + throw new Error('no reserved users found, unable to set native realm passwords'); } - return acc; - }, []); + + return usernames; + }); } async isSecurityEnabled() { @@ -125,10 +123,9 @@ exports.NativeRealm = class NativeRealm { throw error; } - this._log.warning( - 'assuming [elastic] user not available yet, waiting 1.5 seconds and trying again' - ); - await new Promise(resolve => setTimeout(resolve, 1500)); + const sec = 1.5 * attempt; + this._log.warning(`assuming ES isn't initialized completely, trying again in ${sec} seconds`); + await new Promise(resolve => setTimeout(resolve, sec * 1000)); return await this._autoRetry(fn, attempt + 1); } } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx index 247af7e20d581..c727dcd3e3c65 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx @@ -33,6 +33,7 @@ import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../../../../../plugins/kibana_utils/public'; import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public'; import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { META_FIELDS_SETTING } from '../../../../../../../../../plugins/data/common'; import { createEditIndexPatternPageStateContainer } from '../edit_index_pattern_state_container'; import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; import { SourceFiltersTable } from '../source_filters_table'; @@ -98,7 +99,7 @@ export function Tabs({ config, indexPattern, fields, services, history, location }, [indexPattern, indexPattern.fields, refreshFilters]); const fieldWildcardMatcherDecorated = useCallback( - (filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')), + (filters: string[]) => fieldWildcardMatcher(filters, config.get(META_FIELDS_SETTING)), [config] ); diff --git a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js index 1a040e236351a..275f1cb687e22 100644 --- a/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js +++ b/src/legacy/core_plugins/kibana/server/ui_setting_defaults.js @@ -271,106 +271,6 @@ export function getUiSettingDefaults() { defaultMessage: 'The index to access if no index is set', }), }, - defaultColumns: { - name: i18n.translate('kbn.advancedSettings.defaultColumnsTitle', { - defaultMessage: 'Default columns', - }), - value: ['_source'], - description: i18n.translate('kbn.advancedSettings.defaultColumnsText', { - defaultMessage: 'Columns displayed by default in the Discovery tab', - }), - category: ['discover'], - }, - metaFields: { - name: i18n.translate('kbn.advancedSettings.metaFieldsTitle', { - defaultMessage: 'Meta fields', - }), - value: ['_source', '_id', '_type', '_index', '_score'], - description: i18n.translate('kbn.advancedSettings.metaFieldsText', { - defaultMessage: - 'Fields that exist outside of _source to merge into our document when displaying it', - }), - }, - 'discover:sampleSize': { - name: i18n.translate('kbn.advancedSettings.discover.sampleSizeTitle', { - defaultMessage: 'Number of rows', - }), - value: 500, - description: i18n.translate('kbn.advancedSettings.discover.sampleSizeText', { - defaultMessage: 'The number of rows to show in the table', - }), - category: ['discover'], - }, - 'discover:aggs:terms:size': { - name: i18n.translate('kbn.advancedSettings.discover.aggsTermsSizeTitle', { - defaultMessage: 'Number of terms', - }), - value: 20, - type: 'number', - description: i18n.translate('kbn.advancedSettings.discover.aggsTermsSizeText', { - defaultMessage: - 'Determines how many terms will be visualized when clicking the "visualize" ' + - 'button, in the field drop downs, in the discover sidebar.', - }), - category: ['discover'], - }, - 'discover:sort:defaultOrder': { - name: i18n.translate('kbn.advancedSettings.discover.sortDefaultOrderTitle', { - defaultMessage: 'Default sort direction', - }), - value: 'desc', - options: ['desc', 'asc'], - optionLabels: { - desc: i18n.translate('kbn.advancedSettings.discover.sortOrderDesc', { - defaultMessage: 'Descending', - }), - asc: i18n.translate('kbn.advancedSettings.discover.sortOrderAsc', { - defaultMessage: 'Ascending', - }), - }, - type: 'select', - description: i18n.translate('kbn.advancedSettings.discover.sortDefaultOrderText', { - defaultMessage: - 'Controls the default sort direction for time based index patterns in the Discover app.', - }), - category: ['discover'], - }, - 'discover:searchOnPageLoad': { - name: i18n.translate('kbn.advancedSettings.discover.searchOnPageLoadTitle', { - defaultMessage: 'Search on page load', - }), - value: true, - type: 'boolean', - description: i18n.translate('kbn.advancedSettings.discover.searchOnPageLoadText', { - defaultMessage: - 'Controls whether a search is executed when Discover first loads. This setting does not ' + - 'have an effect when loading a saved search.', - }), - category: ['discover'], - }, - 'doc_table:highlight': { - name: i18n.translate('kbn.advancedSettings.docTableHighlightTitle', { - defaultMessage: 'Highlight results', - }), - value: true, - description: i18n.translate('kbn.advancedSettings.docTableHighlightText', { - defaultMessage: - 'Highlight results in Discover and Saved Searches Dashboard. ' + - 'Highlighting makes requests slow when working on big documents.', - }), - category: ['discover'], - }, - 'doc_table:hideTimeColumn': { - name: i18n.translate('kbn.advancedSettings.docTableHideTimeColumnTitle', { - defaultMessage: "Hide 'Time' column", - }), - value: false, - description: i18n.translate('kbn.advancedSettings.docTableHideTimeColumnText', { - defaultMessage: - "Hide the 'Time' column in Discover and in all Saved Searches on Dashboards.", - }), - category: ['discover'], - }, 'courier:ignoreFilterIfFieldNotInIndex': { name: i18n.translate('kbn.advancedSettings.courier.ignoreFilterTitle', { defaultMessage: 'Ignore filter(s)', @@ -484,15 +384,6 @@ export function getUiSettingDefaults() { value: false, category: ['search'], }, - 'fields:popularLimit': { - name: i18n.translate('kbn.advancedSettings.fieldsPopularLimitTitle', { - defaultMessage: 'Popular fields limit', - }), - value: 10, - description: i18n.translate('kbn.advancedSettings.fieldsPopularLimitText', { - defaultMessage: 'The top N most popular fields to show', - }), - }, 'histogram:barTarget': { name: i18n.translate('kbn.advancedSettings.histogram.barTargetTitle', { defaultMessage: 'Target bars', @@ -602,56 +493,6 @@ export function getUiSettingDefaults() { }), category: ['visualization'], }, - 'visualization:colorMapping': { - name: i18n.translate('kbn.advancedSettings.visualization.colorMappingTitle', { - defaultMessage: 'Color mapping', - }), - value: JSON.stringify({ - Count: '#00A69B', - }), - type: 'json', - description: i18n.translate('kbn.advancedSettings.visualization.colorMappingText', { - defaultMessage: 'Maps values to specified colors within visualizations', - }), - category: ['visualization'], - }, - 'visualization:loadingDelay': { - name: i18n.translate('kbn.advancedSettings.visualization.loadingDelayTitle', { - defaultMessage: 'Loading delay', - }), - value: '2s', - description: i18n.translate('kbn.advancedSettings.visualization.loadingDelayText', { - defaultMessage: 'Time to wait before dimming visualizations during query', - }), - category: ['visualization'], - }, - 'visualization:dimmingOpacity': { - name: i18n.translate('kbn.advancedSettings.visualization.dimmingOpacityTitle', { - defaultMessage: 'Dimming opacity', - }), - value: 0.5, - type: 'number', - description: i18n.translate('kbn.advancedSettings.visualization.dimmingOpacityText', { - defaultMessage: - 'The opacity of the chart items that are dimmed when highlighting another element of the chart. ' + - 'The lower this number, the more the highlighted element will stand out. ' + - 'This must be a number between 0 and 1.', - }), - category: ['visualization'], - }, - 'visualization:heatmap:maxBuckets': { - name: i18n.translate('kbn.advancedSettings.visualization.heatmap.maxBucketsTitle', { - defaultMessage: 'Heatmap maximum buckets', - }), - value: 50, - type: 'number', - description: i18n.translate('kbn.advancedSettings.visualization.heatmap.maxBucketsText', { - defaultMessage: - 'The maximum number of buckets a single datasource can return. ' + - 'A higher number might have negative impact on browser rendering performance', - }), - category: ['visualization'], - }, 'csv:separator': { name: i18n.translate('kbn.advancedSettings.csv.separatorTitle', { defaultMessage: 'CSV separator', @@ -831,26 +672,6 @@ export function getUiSettingDefaults() { }, }), }, - 'savedObjects:perPage': { - name: i18n.translate('kbn.advancedSettings.savedObjects.perPageTitle', { - defaultMessage: 'Objects per page', - }), - value: 20, - type: 'number', - description: i18n.translate('kbn.advancedSettings.savedObjects.perPageText', { - defaultMessage: 'Number of objects to show per page in the load dialog', - }), - }, - 'savedObjects:listingLimit': { - name: i18n.translate('kbn.advancedSettings.savedObjects.listingLimitTitle', { - defaultMessage: 'Objects listing limit', - }), - type: 'number', - value: 1000, - description: i18n.translate('kbn.advancedSettings.savedObjects.listingLimitText', { - defaultMessage: 'Number of objects to fetch for the listing pages', - }), - }, 'timepicker:timeDefaults': { name: i18n.translate('kbn.advancedSettings.timepicker.timeDefaultsTitle', { defaultMessage: 'Time filter defaults', @@ -1097,15 +918,6 @@ export function getUiSettingDefaults() { type: 'number', category: ['notifications'], }, - 'metrics:max_buckets': { - name: i18n.translate('kbn.advancedSettings.maxBucketsTitle', { - defaultMessage: 'Maximum buckets', - }), - value: 2000, - description: i18n.translate('kbn.advancedSettings.maxBucketsText', { - defaultMessage: 'The maximum number of buckets a single datasource can return', - }), - }, 'state:storeInSessionStorage': { name: i18n.translate('kbn.advancedSettings.storeUrlTitle', { defaultMessage: 'Store URLs in session storage', @@ -1128,38 +940,6 @@ export function getUiSettingDefaults() { 'The placeholder for the "Index pattern name" field in "Management > Index Patterns > Create Index Pattern".', }), }, - 'context:defaultSize': { - name: i18n.translate('kbn.advancedSettings.context.defaultSizeTitle', { - defaultMessage: 'Context size', - }), - value: 5, - description: i18n.translate('kbn.advancedSettings.context.defaultSizeText', { - defaultMessage: 'The number of surrounding entries to show in the context view', - }), - category: ['discover'], - }, - 'context:step': { - name: i18n.translate('kbn.advancedSettings.context.sizeStepTitle', { - defaultMessage: 'Context size step', - }), - value: 5, - description: i18n.translate('kbn.advancedSettings.context.sizeStepText', { - defaultMessage: 'The step size to increment or decrement the context size by', - }), - category: ['discover'], - }, - 'context:tieBreakerFields': { - name: i18n.translate('kbn.advancedSettings.context.tieBreakerFieldsTitle', { - defaultMessage: 'Tie breaker fields', - }), - value: ['_doc'], - description: i18n.translate('kbn.advancedSettings.context.tieBreakerFieldsText', { - defaultMessage: - 'A comma-separated list of fields to use for tie-breaking between documents that have the same timestamp value. ' + - 'From this list the first field that is present and sortable in the current index pattern is used.', - }), - category: ['discover'], - }, 'accessibility:disableAnimations': { name: i18n.translate('kbn.advancedSettings.disableAnimationsTitle', { defaultMessage: 'Disable Animations', diff --git a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js index fb54c36df27d7..ee729d2b427ad 100644 --- a/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js +++ b/src/legacy/core_plugins/timelion/public/directives/saved_object_finder.js @@ -28,6 +28,7 @@ import { PaginateControlsDirectiveProvider, PaginateDirectiveProvider, } from '../../../../../plugins/kibana_legacy/public'; +import { PER_PAGE_SETTING } from '../../../../../plugins/saved_objects/common'; const module = uiModules.get('kibana'); @@ -65,7 +66,7 @@ module const $input = $element.find('input[ng-model=filter]'); // The number of items to show in the list - $scope.perPage = config.get('savedObjects:perPage'); + $scope.perPage = config.get(PER_PAGE_SETTING); // the list that will hold the suggestions const $list = $element.find('ul'); diff --git a/src/plugins/bfetch/server/index.ts b/src/plugins/bfetch/server/index.ts index 06b7c793c537e..a30481c5f5752 100644 --- a/src/plugins/bfetch/server/index.ts +++ b/src/plugins/bfetch/server/index.ts @@ -21,6 +21,7 @@ import { PluginInitializerContext } from '../../../core/server'; import { BfetchServerPlugin } from './plugin'; export { BfetchServerSetup, BfetchServerStart, BatchProcessingRouteParams } from './plugin'; +export { StreamingRequestHandler } from './types'; export function plugin(initializerContext: PluginInitializerContext) { return new BfetchServerPlugin(initializerContext); diff --git a/src/plugins/bfetch/server/mocks.ts b/src/plugins/bfetch/server/mocks.ts index e0a76ba8da325..5a772d641493d 100644 --- a/src/plugins/bfetch/server/mocks.ts +++ b/src/plugins/bfetch/server/mocks.ts @@ -28,6 +28,7 @@ const createSetupContract = (): Setup => { const setupContract: Setup = { addBatchProcessingRoute: jest.fn(), addStreamingResponseRoute: jest.fn(), + createStreamingRequestHandler: jest.fn(), }; return setupContract; }; diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index d2ea52f23bc7d..0502781e34ce2 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -24,6 +24,8 @@ import { Plugin, Logger, KibanaRequest, + RouteMethod, + RequestHandler, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { Subject } from 'rxjs'; @@ -35,6 +37,7 @@ import { removeLeadingSlash, normalizeError, } from '../common'; +import { StreamingRequestHandler } from './types'; import { createNDJSONStream } from './streaming'; // eslint-disable-next-line @@ -47,6 +50,7 @@ export interface BatchProcessingRouteParams { onBatchItem: (data: BatchItemData) => Promise; } +/** @public */ export interface BfetchServerSetup { addBatchProcessingRoute: ( path: string, @@ -56,11 +60,48 @@ export interface BfetchServerSetup { path: string, params: (request: KibanaRequest) => StreamingResponseHandler ) => void; + /** + * Create a streaming request handler to be able to use an Observable to return chunked content to the client. + * This is meant to be used with the `fetchStreaming` API of the `bfetch` client-side plugin. + * + * @example + * ```ts + * setup({ http }: CoreStart, { bfetch }: SetupDeps) { + * const router = http.createRouter(); + * router.post( + * { + * path: '/api/my-plugin/stream-endpoint, + * validate: { + * body: schema.object({ + * term: schema.string(), + * }), + * } + * }, + * bfetch.createStreamingResponseHandler(async (ctx, req) => { + * const { term } = req.body; + * const results$ = await myApi.getResults$(term); + * return results$; + * }) + * )} + * + * ``` + * + * @param streamHandler + */ + createStreamingRequestHandler: ( + streamHandler: StreamingRequestHandler + ) => RequestHandler; } // eslint-disable-next-line export interface BfetchServerStart {} +const streamingHeaders = { + 'Content-Type': 'application/x-ndjson', + Connection: 'keep-alive', + 'Transfer-Encoding': 'chunked', +}; + export class BfetchServerPlugin implements Plugin< @@ -76,10 +117,12 @@ export class BfetchServerPlugin const router = core.http.createRouter(); const addStreamingResponseRoute = this.addStreamingResponseRoute({ router, logger }); const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); + const createStreamingRequestHandler = this.createStreamingRequestHandler({ logger }); return { addBatchProcessingRoute, addStreamingResponseRoute, + createStreamingRequestHandler, }; } @@ -106,19 +149,30 @@ export class BfetchServerPlugin async (context, request, response) => { const handlerInstance = handler(request); const data = request.body; - const headers = { - 'Content-Type': 'application/x-ndjson', - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked', - }; return response.ok({ - headers, - body: createNDJSONStream(data, handlerInstance, logger), + headers: streamingHeaders, + body: createNDJSONStream(handlerInstance.getResponseStream(data), logger), }); } ); }; + private createStreamingRequestHandler = ({ + logger, + }: { + logger: Logger; + }): BfetchServerSetup['createStreamingRequestHandler'] => streamHandler => async ( + context, + request, + response + ) => { + const response$ = await streamHandler(context, request); + return response.ok({ + headers: streamingHeaders, + body: createNDJSONStream(response$, logger), + }); + }; + private addBatchProcessingRoute = ( addStreamingResponseRoute: BfetchServerSetup['addStreamingResponseRoute'] ): BfetchServerSetup['addBatchProcessingRoute'] => < diff --git a/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts b/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts index 82fe31906e8bf..c567784becd16 100644 --- a/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts +++ b/src/plugins/bfetch/server/streaming/create_ndjson_stream.ts @@ -17,19 +17,17 @@ * under the License. */ +import { Observable } from 'rxjs'; import { Logger } from 'src/core/server'; import { Stream, PassThrough } from 'stream'; -import { StreamingResponseHandler } from '../../common/types'; const delimiter = '\n'; -export const createNDJSONStream = ( - payload: Payload, - handler: StreamingResponseHandler, +export const createNDJSONStream = ( + results: Observable, logger: Logger ): Stream => { const stream = new PassThrough(); - const results = handler.getResponseStream(payload); results.subscribe({ next: (message: Response) => { diff --git a/src/plugins/bfetch/server/types.ts b/src/plugins/bfetch/server/types.ts new file mode 100644 index 0000000000000..c05822331d866 --- /dev/null +++ b/src/plugins/bfetch/server/types.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { KibanaRequest, RequestHandlerContext, RouteMethod } from 'kibana/server'; + +/** + * Request handler modified to allow to return an observable. + * + * See {@link BfetchServerSetup.createStreamingRequestHandler} for usage example. + * @public + */ +export type StreamingRequestHandler< + Response = unknown, + P = unknown, + Q = unknown, + B = unknown, + Method extends RouteMethod = any +> = ( + context: RequestHandlerContext, + request: KibanaRequest +) => Observable | Promise>; diff --git a/src/plugins/charts/common/index.ts b/src/plugins/charts/common/index.ts new file mode 100644 index 0000000000000..1ebf3bcb8f4b6 --- /dev/null +++ b/src/plugins/charts/common/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const COLOR_MAPPING_SETTING = 'visualization:colorMapping'; diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json index 43c41fe64306a..9f4433e7099d8 100644 --- a/src/plugins/charts/kibana.json +++ b/src/plugins/charts/kibana.json @@ -1,6 +1,6 @@ { "id": "charts", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/charts/public/services/colors/colors.test.ts b/src/plugins/charts/public/services/colors/colors.test.ts index 3e9012cd71dc5..e3f99f2407f75 100644 --- a/src/plugins/charts/public/services/colors/colors.test.ts +++ b/src/plugins/charts/public/services/colors/colors.test.ts @@ -18,6 +18,7 @@ */ import { coreMock } from '../../../../../core/public/mocks'; +import { COLOR_MAPPING_SETTING } from '../../../common'; import { seedColors } from './seed_colors'; import { ColorsService } from './colors'; @@ -44,13 +45,13 @@ describe('Vislib Color Service', () => { const nullValue = null; beforeEach(() => { - previousConfig = config.get('visualization:colorMapping'); - config.set('visualization:colorMapping', {}); + previousConfig = config.get(COLOR_MAPPING_SETTING); + config.set(COLOR_MAPPING_SETTING, {}); color = colors.createColorLookupFunction(arr, {}); }); afterEach(() => { - config.set('visualization:colorMapping', previousConfig); + config.set(COLOR_MAPPING_SETTING, previousConfig); }); it('should throw error if not initialized', () => { diff --git a/src/plugins/charts/public/services/colors/mapped_colors.test.ts b/src/plugins/charts/public/services/colors/mapped_colors.test.ts index 77f1faf468b46..c3b9b0909051c 100644 --- a/src/plugins/charts/public/services/colors/mapped_colors.test.ts +++ b/src/plugins/charts/public/services/colors/mapped_colors.test.ts @@ -21,6 +21,7 @@ import _ from 'lodash'; import d3 from 'd3'; import { coreMock } from '../../../../../core/public/mocks'; +import { COLOR_MAPPING_SETTING } from '../../../common'; import { seedColors } from './seed_colors'; import { MappedColors } from './mapped_colors'; @@ -36,16 +37,16 @@ describe('Mapped Colors', () => { let previousConfig: any; beforeEach(() => { - previousConfig = config.get('visualization:colorMapping'); + previousConfig = config.get(COLOR_MAPPING_SETTING); mappedColors.purge(); }); afterEach(() => { - config.set('visualization:colorMapping', previousConfig); + config.set(COLOR_MAPPING_SETTING, previousConfig); }); it('should properly map keys to unique colors', () => { - config.set('visualization:colorMapping', {}); + config.set(COLOR_MAPPING_SETTING, {}); const arr = [1, 2, 3, 4, 5]; mappedColors.mapKeys(arr); @@ -59,7 +60,7 @@ describe('Mapped Colors', () => { it('should not include colors used by the config', () => { const newConfig = { bar: seedColors[0] }; - config.set('visualization:colorMapping', newConfig); + config.set(COLOR_MAPPING_SETTING, newConfig); const arr = ['foo', 'baz', 'qux']; mappedColors.mapKeys(arr); @@ -71,7 +72,7 @@ describe('Mapped Colors', () => { it('should create a unique array of colors even when config is set', () => { const newConfig = { bar: seedColors[0] }; - config.set('visualization:colorMapping', newConfig); + config.set(COLOR_MAPPING_SETTING, newConfig); const arr = ['foo', 'bar', 'baz', 'qux']; mappedColors.mapKeys(arr); @@ -92,7 +93,7 @@ describe('Mapped Colors', () => { const color = d3.rgb(seedColors[0]); const rgb = `rgb(${color.r}, ${color.g}, ${color.b})`; const newConfig = { bar: rgb }; - config.set('visualization:colorMapping', newConfig); + config.set(COLOR_MAPPING_SETTING, newConfig); const arr = ['foo', 'bar', 'baz', 'qux']; mappedColors.mapKeys(arr); diff --git a/src/plugins/charts/public/services/colors/mapped_colors.ts b/src/plugins/charts/public/services/colors/mapped_colors.ts index 1c6ed690df632..1469d357e7e79 100644 --- a/src/plugins/charts/public/services/colors/mapped_colors.ts +++ b/src/plugins/charts/public/services/colors/mapped_colors.ts @@ -22,6 +22,7 @@ import d3 from 'd3'; import { CoreSetup } from 'kibana/public'; +import { COLOR_MAPPING_SETTING } from '../../../common'; import { createColorPalette } from './color_palette'; const standardizeColor = (color: string) => d3.rgb(color).toString(); @@ -41,7 +42,7 @@ export class MappedColors { } private getConfigColorMapping() { - return _.mapValues(this.uiSettings.get('visualization:colorMapping'), standardizeColor); + return _.mapValues(this.uiSettings.get(COLOR_MAPPING_SETTING), standardizeColor); } public get oldMap(): any { diff --git a/src/plugins/charts/server/index.ts b/src/plugins/charts/server/index.ts new file mode 100644 index 0000000000000..75a57ab6b405c --- /dev/null +++ b/src/plugins/charts/server/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ChartsServerPlugin } from './plugin'; + +export const plugin = () => new ChartsServerPlugin(); diff --git a/src/plugins/charts/server/plugin.ts b/src/plugins/charts/server/plugin.ts new file mode 100644 index 0000000000000..6bf45fb804469 --- /dev/null +++ b/src/plugins/charts/server/plugin.ts @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { CoreSetup, Plugin } from 'kibana/server'; +import { COLOR_MAPPING_SETTING } from '../common'; + +export class ChartsServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register({ + [COLOR_MAPPING_SETTING]: { + name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', { + defaultMessage: 'Color mapping', + }), + value: JSON.stringify({ + Count: '#00A69B', + }), + type: 'json', + description: i18n.translate('charts.advancedSettings.visualization.colorMappingText', { + defaultMessage: 'Maps values to specified colors within visualizations', + }), + category: ['visualization'], + schema: schema.string(), + }, + }); + + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 46df7036c09cd..8e29d2734065e 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -25,7 +25,6 @@ import angular, { IModule } from 'angular'; import 'angular-sanitize'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { - AppMountContext, ChromeStart, IUiSettingsClient, CoreStart, @@ -42,7 +41,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../navigat import { DataPublicPluginStart } from '../../../data/public'; import { SharePluginStart } from '../../../share/public'; import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public'; -import { SavedObjectLoader } from '../../../saved_objects/public'; +import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; // required for i18nIdDirective import 'angular-sanitize'; @@ -73,13 +72,14 @@ export interface RenderDeps { usageCollection?: UsageCollectionSetup; navigateToDefaultApp: KibanaLegacyStart['navigateToDefaultApp']; scopedHistory: () => ScopedHistory; + savedObjects: SavedObjectsStart; } let angularModuleInstance: IModule | null = null; export const renderApp = (element: HTMLElement, appBasePath: string, deps: RenderDeps) => { if (!angularModuleInstance) { - angularModuleInstance = createLocalAngularModule(deps.core, deps.navigation); + angularModuleInstance = createLocalAngularModule(); // global routing stuff configureAppAngularModule( angularModuleInstance, @@ -119,7 +119,7 @@ function mountDashboardApp(appBasePath: string, element: HTMLElement) { return $injector; } -function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) { +function createLocalAngularModule() { createLocalI18nModule(); createLocalIconModule(); diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index d12d9de8c7dd4..4e3cc15d93ece 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -29,7 +29,6 @@ import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constan import { createKbnUrlStateStorage, redirectWhenMissing, - InvalidJSONProperty, SavedObjectNotFound, } from '../../../kibana_utils/public'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; @@ -50,6 +49,7 @@ export function initDashboardApp(app, deps) { ['listingLimit', { watchDepth: 'reference' }], ['hideWriteControls', { watchDepth: 'reference' }], ['initialFilter', { watchDepth: 'reference' }], + ['initialPageSize', { watchDepth: 'reference' }], ]); }); @@ -109,7 +109,8 @@ export function initDashboardApp(app, deps) { kbnUrlStateStorage ); - $scope.listingLimit = deps.uiSettings.get('savedObjects:listingLimit'); + $scope.listingLimit = deps.savedObjects.settings.getListingLimit(); + $scope.initialPageSize = deps.savedObjects.settings.getPerPage(); $scope.create = () => { history.push(DashboardConstants.CREATE_NEW_DASHBOARD_URL); }; @@ -213,13 +214,6 @@ export function initDashboardApp(app, deps) { return savedDashboard; }) .catch(error => { - // A corrupt dashboard was detected (e.g. with invalid JSON properties) - if (error instanceof InvalidJSONProperty) { - deps.core.notifications.toasts.addDanger(error.message); - history.push(DashboardConstants.LANDING_PAGE_PATH); - return; - } - // Preserve BWC of v5.3.0 links for new, unsaved dashboards. // See https://github.com/elastic/kibana/issues/10951 for more context. if (error instanceof SavedObjectNotFound && id === 'create') { @@ -237,19 +231,12 @@ export function initDashboardApp(app, deps) { ); return new Promise(() => {}); } else { - throw error; + // E.g. a corrupt or deleted dashboard + deps.core.notifications.toasts.addDanger(error.message); + history.push(DashboardConstants.LANDING_PAGE_PATH); + return new Promise(() => {}); } - }) - .catch( - redirectWhenMissing({ - history, - navigateToApp: deps.core.application.navigateToApp, - mapping: { - dashboard: DashboardConstants.LANDING_PAGE_PATH, - }, - toastNotifications: deps.core.notifications.toasts, - }) - ); + }); }, }, }) diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.js b/src/plugins/dashboard/public/application/listing/dashboard_listing.js index c24e7e4617806..c8cb551fbe561 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.js +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.js @@ -50,6 +50,7 @@ export class DashboardListing extends React.Component { tableColumns={this.getTableColumns()} listingLimit={this.props.listingLimit} initialFilter={this.props.initialFilter} + initialPageSize={this.props.initialPageSize} noItemsFragment={this.getNoItemsMessage()} entityName={i18n.translate('dashboard.listing.table.entityName', { defaultMessage: 'dashboard', @@ -187,6 +188,7 @@ DashboardListing.propTypes = { listingLimit: PropTypes.number.isRequired, hideWriteControls: PropTypes.bool.isRequired, initialFilter: PropTypes.string, + initialPageSize: PropTypes.number.isRequired, }; DashboardListing.defaultProps = { diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html index f57c10d1a48dd..ba05c138a0cba 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing_ng_wrapper.html @@ -8,4 +8,5 @@ listing-limit="listingLimit" hide-write-controls="hideWriteControls" initial-filter="initialFilter" + initial-page-size="initialPageSize" > diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 07c9feefa0c02..b4419adfe58da 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,37 +33,29 @@ import { AppUpdater, ScopedHistory, } from 'src/core/public'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UsageCollectionSetup } from '../../usage_collection/public'; +import { CONTEXT_MENU_TRIGGER, EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; +import { SharePluginSetup, SharePluginStart, UrlGeneratorContract } from '../../share/public'; +import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; + +import { Start as InspectorStartContract } from '../../inspector/public'; +import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { - CONTEXT_MENU_TRIGGER, - EmbeddableSetup, - EmbeddableStart, -} from '../../../plugins/embeddable/public'; -import { - DataPublicPluginStart, - DataPublicPluginSetup, - esFilters, -} from '../../../plugins/data/public'; -import { - SharePluginSetup, - SharePluginStart, - UrlGeneratorContract, -} from '../../../plugins/share/public'; -import { UiActionsSetup, UiActionsStart } from '../../../plugins/ui_actions/public'; - -import { Start as InspectorStartContract } from '../../../plugins/inspector/public'; -import { NavigationPublicPluginStart as NavigationStart } from '../../../plugins/navigation/public'; -import { getSavedObjectFinder, SavedObjectLoader } from '../../../plugins/saved_objects/public'; + getSavedObjectFinder, + SavedObjectLoader, + SavedObjectsStart, +} from '../../saved_objects/public'; import { ExitFullScreenButton as ExitFullScreenButtonUi, ExitFullScreenButtonProps, -} from '../../../plugins/kibana_react/public'; -import { createKbnUrlTracker, Storage } from '../../../plugins/kibana_utils/public'; +} from '../../kibana_react/public'; +import { createKbnUrlTracker, Storage } from '../../kibana_utils/public'; import { KibanaLegacySetup, KibanaLegacyStart, initAngularBootstrap, -} from '../../../plugins/kibana_legacy/public'; +} from '../../kibana_legacy/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../../plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; @@ -117,6 +109,7 @@ interface StartDependencies { savedObjectsClient: SavedObjectsClientContract; share?: SharePluginStart; uiActions: UiActionsStart; + savedObjects: SavedObjectsStart; } export type Setup = void; @@ -250,6 +243,7 @@ export class DashboardPlugin share: shareStart, data: dataStart, kibanaLegacy: { dashboardConfig, navigateToDefaultApp }, + savedObjects, } = pluginsStart; const deps: RenderDeps = { @@ -276,6 +270,7 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, scopedHistory: () => this.currentHistory!, + savedObjects, }; // make sure the index pattern list is up to date await dataStart.indexPatterns.clearCache(); diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 00786a0c72cf1..66a96e3e6e129 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -18,3 +18,5 @@ */ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; +export const META_FIELDS_SETTING = 'metaFields'; +export const DOC_HIGHLIGHT_SETTING = 'doc_table:highlight'; diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 4882e8eafc0d3..18853f7e292f6 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -53,16 +53,16 @@ export function selectRangeAction( }); }, isCompatible, - execute: async ({ timeFieldName, data }: SelectRangeActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { + execute: async ({ data }: SelectRangeActionContext) => { + if (!(await isCompatible({ data }))) { throw new IncompatibleActionError(); } const selectedFilters = await createFiltersFromRangeSelectAction(data); - if (timeFieldName) { + if (data.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, + data.timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 210a58b3f75aa..17c1b1b1e1769 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -57,12 +57,12 @@ export function valueClickAction( }); }, isCompatible, - execute: async (context: ValueClickActionContext) => { - if (!(await isCompatible(context))) { + execute: async ({ data }: ValueClickActionContext) => { + if (!(await isCompatible({ data }))) { throw new IncompatibleActionError(); } - const filters: Filter[] = await createFiltersFromValueClickAction(context.data); + const filters: Filter[] = await createFiltersFromValueClickAction(data); let selectedFilters = filters; @@ -98,9 +98,9 @@ export function valueClickAction( selectedFilters = await filterSelectionPromise; } - if (context.timeFieldName) { + if (data.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.timeFieldName, + data.timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts index f3297f21c572a..ecbfceac14b66 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_pattern.ts @@ -28,8 +28,13 @@ import { MappingObject, } from '../../../../kibana_utils/public'; -import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; - +import { + ES_FIELD_TYPES, + KBN_FIELD_TYPES, + IIndexPattern, + IFieldType, + META_FIELDS_SETTING, +} from '../../../common'; import { findByTitle } from '../utils'; import { IndexPatternMissingIndices } from '../lib'; import { Field, IIndexPatternFieldList, getIndexPatternFieldListCreator } from '../fields'; @@ -104,7 +109,7 @@ export class IndexPattern implements IIndexPattern { this.getConfig = getConfig; this.shortDotsEnable = this.getConfig('shortDots:enable'); - this.metaFields = this.getConfig('metaFields'); + this.metaFields = this.getConfig(META_FIELDS_SETTING); this.createFieldList = getIndexPatternFieldListCreator({ fieldFormats: getFieldFormats(), @@ -112,8 +117,8 @@ export class IndexPattern implements IIndexPattern { }); this.fields = this.createFieldList(this, [], this.shortDotsEnable); - this.fieldsFetcher = createFieldsFetcher(this, apiClient, this.getConfig('metaFields')); - this.flattenHit = flattenHitWrapper(this, this.getConfig('metaFields')); + this.fieldsFetcher = createFieldsFetcher(this, apiClient, this.getConfig(META_FIELDS_SETTING)); + this.flattenHit = flattenHitWrapper(this, this.getConfig(META_FIELDS_SETTING)); this.formatHit = formatHitProvider( this, getFieldFormats().getDefaultInstance(KBN_FIELD_TYPES.STRING) diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index acbb193807623..9fdef5e1f3eb0 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -75,6 +75,7 @@ import { CoreStart } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; import { filterDocvalueFields } from './filter_docvalue_fields'; import { fieldWildcardFilter } from '../../../../kibana_utils/public'; +import { META_FIELDS_SETTING, DOC_HIGHLIGHT_SETTING } from '../../../common'; import { IIndexPattern, ISearchGeneric, SearchRequest } from '../..'; import { SearchSourceOptions, SearchSourceFields } from './types'; import { FetchOptions, RequestFailure, getSearchParams, handleResponse } from '../fetch'; @@ -422,7 +423,10 @@ export class SearchSource { if (body._source) { // exclude source fields for this index pattern specified by the user - const filter = fieldWildcardFilter(body._source.excludes, uiSettings.get('metaFields')); + const filter = fieldWildcardFilter( + body._source.excludes, + uiSettings.get(META_FIELDS_SETTING) + ); body.docvalue_fields = body.docvalue_fields.filter((docvalueField: any) => filter(docvalueField.field) ); @@ -444,7 +448,7 @@ export class SearchSource { body.query = buildEsQuery(index, query, filters, esQueryConfigs); if (highlightAll && body.query) { - body.highlight = getHighlightRequest(body.query, uiSettings.get('doc_table:highlight')); + body.highlight = getHighlightRequest(body.query, uiSettings.get(DOC_HIGHLIGHT_SETTING)); delete searchRequest.highlightAll; } diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index efb8759e7bead..83a5358642ce4 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -27,6 +27,7 @@ import { KqlTelemetryService } from './kql_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { AutocompleteService } from './autocomplete'; import { FieldFormatsService, FieldFormatsSetup, FieldFormatsStart } from './field_formats'; +import { uiSettings } from './ui_settings'; export interface DataPluginSetup { search: ISearchSetup; @@ -63,6 +64,7 @@ export class DataServerPlugin implements Plugin = (context: ISearc // src/plugins/data/server/index.ts:185:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:186:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:189:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/plugin.ts:64:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/plugin.ts:65:14 - (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts new file mode 100644 index 0000000000000..5af62be295201 --- /dev/null +++ b/src/plugins/data/server/ui_settings.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { UiSettingsParams } from 'kibana/server'; +import { META_FIELDS_SETTING, DOC_HIGHLIGHT_SETTING } from '../common'; + +export const uiSettings: Record = { + [META_FIELDS_SETTING]: { + name: i18n.translate('data.advancedSettings.metaFieldsTitle', { + defaultMessage: 'Meta fields', + }), + value: ['_source', '_id', '_type', '_index', '_score'], + description: i18n.translate('data.advancedSettings.metaFieldsText', { + defaultMessage: + 'Fields that exist outside of _source to merge into our document when displaying it', + }), + schema: schema.arrayOf(schema.string()), + }, + [DOC_HIGHLIGHT_SETTING]: { + name: i18n.translate('data.advancedSettings.docTableHighlightTitle', { + defaultMessage: 'Highlight results', + }), + value: true, + description: i18n.translate('data.advancedSettings.docTableHighlightText', { + defaultMessage: + 'Highlight results in Discover and Saved Searches Dashboard. ' + + 'Highlighting makes requests slow when working on big documents.', + }), + category: ['discover'], + schema: schema.boolean(), + }, +}; diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts new file mode 100644 index 0000000000000..72030d91220b5 --- /dev/null +++ b/src/plugins/discover/common/index.ts @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; +export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; +export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; +export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder'; +export const SEARCH_ON_PAGE_LOAD_SETTING = 'discover:searchOnPageLoad'; +export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn'; +export const FIELDS_LIMIT_SETTING = 'fields:popularLimit'; +export const CONTEXT_DEFAULT_SIZE_SETTING = 'context:defaultSize'; +export const CONTEXT_STEP_SETTING = 'context:step'; +export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index 6889eceb9ac17..f395f28a428fc 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -19,6 +19,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../common'; import { getAngularModule, getServices } from '../../kibana_services'; import './context_app'; import { getState } from './context_state'; @@ -78,7 +79,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { setAppState, flushToUrl, } = getState({ - defaultStepSize: getServices().uiSettings.get('context:defaultSize'), + defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), timeFieldName: indexPattern.timeFieldName, storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), history: getServices().history(), diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index c4a42642f7fd5..e79689d3e26e3 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -18,6 +18,7 @@ */ import _ from 'lodash'; +import { CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../common'; import { getAngularModule, getServices } from '../../kibana_services'; import contextAppTemplate from './context_app.html'; import './context/components/action_bar'; @@ -61,8 +62,8 @@ function ContextAppController($scope, Private) { const queryParameterActions = getQueryParameterActions(filterManager, indexpatterns); const queryActions = Private(QueryActionsProvider); this.state = createInitialState( - parseInt(uiSettings.get('context:step'), 10), - getFirstSortableField(this.indexPattern, uiSettings.get('context:tieBreakerFields')) + parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10), + getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)) ); this.actions = _.mapValues( diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 69dec63ae57ba..e2d434074afdb 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -75,6 +75,13 @@ import { } from '../../../../data/public'; import { getIndexPatternId } from '../helpers/get_index_pattern_id'; import { addFatalError } from '../../../../kibana_legacy/public'; +import { + DEFAULT_COLUMNS_SETTING, + SAMPLE_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, + SEARCH_ON_PAGE_LOAD_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, +} from '../../../common'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -554,7 +561,7 @@ function discoverController( const { searchFields, selectFields } = await getSharingDataFields( $scope.state.columns, $scope.indexPattern.timeFieldName, - config.get('doc_table:hideTimeColumn') + config.get(DOC_HIDE_TIME_COLUMN_SETTING) ); searchSource.setField('fields', searchFields); searchSource.setField( @@ -562,7 +569,7 @@ function discoverController( getSortForSearchSource( $scope.state.sort, $scope.indexPattern, - config.get('discover:sort:defaultOrder') + config.get(SORT_DEFAULT_ORDER_SETTING) ) ); searchSource.setField('highlight', null); @@ -595,7 +602,9 @@ function discoverController( query, sort: getSortArray(savedSearch.sort, $scope.indexPattern), columns: - savedSearch.columns.length > 0 ? savedSearch.columns : config.get('defaultColumns').slice(), + savedSearch.columns.length > 0 + ? savedSearch.columns + : config.get(DEFAULT_COLUMNS_SETTING).slice(), index: $scope.indexPattern.id, interval: 'auto', filters: _.cloneDeep($scope.searchSource.getOwnField('filter')), @@ -625,7 +634,7 @@ function discoverController( $scope.opts = { // number of records to fetch, then paginate through - sampleSize: config.get('discover:sampleSize'), + sampleSize: config.get(SAMPLE_SIZE_SETTING), timefield: getTimeField(), savedSearch: savedSearch, indexPatternList: $route.current.locals.savedObjects.ip.list, @@ -635,7 +644,7 @@ function discoverController( // A saved search is created on every page load, so we check the ID to see if we're loading a // previously saved search or if it is just transient return ( - config.get('discover:searchOnPageLoad') || + config.get(SEARCH_ON_PAGE_LOAD_SETTING) || savedSearch.id !== undefined || timefilter.getRefreshInterval().pause === false ); @@ -946,7 +955,7 @@ function discoverController( getSortForSearchSource( $scope.state.sort, indexPattern, - config.get('discover:sort:defaultOrder') + config.get(SORT_DEFAULT_ORDER_SETTING) ) ) .setField('query', $scope.state.query || null) diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts index 5e7ad6a1d1ea0..60dfb69e85e74 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header.ts @@ -18,6 +18,7 @@ */ import { TableHeader } from './table_header/table_header'; import { getServices } from '../../../../kibana_services'; +import { SORT_DEFAULT_ORDER_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; export function createTableHeaderDirective(reactDirective: any) { const { uiSettings: config } = getServices(); @@ -36,9 +37,9 @@ export function createTableHeaderDirective(reactDirective: any) { ], { restrict: 'A' }, { - hideTimeColumn: config.get('doc_table:hideTimeColumn'), + hideTimeColumn: config.get(DOC_HIDE_TIME_COLUMN_SETTING, false), isShortDots: config.get('shortDots:enable'), - defaultSortOrder: config.get('discover:sort:defaultOrder'), + defaultSortOrder: config.get(SORT_DEFAULT_ORDER_SETTING, 'desc'), } ); } diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index 5fa37a5ac28eb..fbbbabdbe7bc2 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -29,6 +29,7 @@ import openRowHtml from './table_row/open.html'; import detailsHtml from './table_row/details.html'; import { dispatchRenderComplete } from '../../../../../../kibana_utils/public'; +import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../../common'; import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; import { esFilters } from '../../../../../../data/public'; @@ -137,7 +138,7 @@ export function createTableRowDirective($compile: ng.ICompileService, $httpParam const newHtmls = [openRowHtml]; const mapping = indexPattern.fields.getByName; - const hideTimeColumn = getServices().uiSettings.get('doc_table:hideTimeColumn'); + const hideTimeColumn = getServices().uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); if (indexPattern.timeFieldName && !hideTimeColumn) { newHtmls.push( cellTemplate({ diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts index 8af7380afcdc9..70f277543a52a 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts @@ -19,6 +19,7 @@ import html from './doc_table.html'; import { dispatchRenderComplete } from '../../../../../kibana_utils/public'; +import { SAMPLE_SIZE_SETTING } from '../../../../common'; // @ts-ignore import { getLimitedSearchResultsMessage } from './doc_table_strings'; import { getServices } from '../../../kibana_services'; @@ -65,7 +66,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) { }; $scope.limitedResultsWarning = getLimitedSearchResultsMessage( - getServices().uiSettings.get('discover:sampleSize') + getServices().uiSettings.get(SAMPLE_SIZE_SETTING, 500) ); $scope.addRows = function() { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 56597dd31e572..0d9aebe11ece6 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -27,6 +27,7 @@ import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; import { IndexPatternAttributes } from '../../../../../data/common'; import { SavedObject } from '../../../../../../core/types'; +import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IIndexPatternFieldList, @@ -131,7 +132,7 @@ export function DiscoverSidebar({ [selectedIndexPattern, state, columns, hits, services] ); - const popularLimit = services.uiSettings.get('fields:popularLimit'); + const popularLimit = services.uiSettings.get(FIELDS_LIMIT_SETTING); const useShortDots = services.uiSettings.get('shortDots:enable'); const { diff --git a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts index 6b2d20ae8c910..d585c5d6f2638 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/visualize_url_utils.ts @@ -28,6 +28,7 @@ import { import { AppState } from '../../../angular/discover_state'; import { DiscoverServices } from '../../../../build_services'; import { VisualizationsStart, VisTypeAlias } from '../../../../../../visualizations/public'; +import { AGGS_TERMS_SIZE_SETTING } from '../../../../../common'; export function isMapsAppRegistered(visualizations: VisualizationsStart) { return visualizations.getAliases().some(({ name }: VisTypeAlias) => { @@ -114,7 +115,7 @@ export function getVisualizeUrl( columns: string[], services: DiscoverServices ) { - const aggsTermSize = services.uiSettings.get('discover:aggs:terms:size'); + const aggsTermSize = services.uiSettings.get(AGGS_TERMS_SIZE_SETTING); const urlParams = parse(services.history().location.search) as Record; if ( diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index f1bda5fcae10b..ed70c90eb64e6 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -48,6 +48,7 @@ import { } from '../../kibana_services'; import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; +import { SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; interface SearchScope extends ng.IScope { columns?: string[]; @@ -273,13 +274,13 @@ export class SearchEmbeddable extends Embeddable if (this.abortController) this.abortController.abort(); this.abortController = new AbortController(); - searchSource.setField('size', getServices().uiSettings.get('discover:sampleSize')); + searchSource.setField('size', getServices().uiSettings.get(SAMPLE_SIZE_SETTING)); searchSource.setField( 'sort', getSortForSearchSource( this.searchScope.sort, this.searchScope.indexPattern, - getServices().uiSettings.get('discover:sort:defaultOrder') + getServices().uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ) ); diff --git a/src/plugins/discover/server/index.ts b/src/plugins/discover/server/index.ts index 15a948c56148e..20ccc8b182078 100644 --- a/src/plugins/discover/server/index.ts +++ b/src/plugins/discover/server/index.ts @@ -17,8 +17,6 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/server'; import { DiscoverServerPlugin } from './plugin'; -export const plugin = (initContext: PluginInitializerContext) => - new DiscoverServerPlugin(initContext); +export const plugin = () => new DiscoverServerPlugin(); diff --git a/src/plugins/discover/server/plugin.ts b/src/plugins/discover/server/plugin.ts index 04502f5fc14e6..a7445a5189163 100644 --- a/src/plugins/discover/server/plugin.ts +++ b/src/plugins/discover/server/plugin.ts @@ -17,26 +17,19 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; +import { uiSettings } from './ui_settings'; import { capabilitiesProvider } from './capabilities_provider'; export class DiscoverServerPlugin implements Plugin { - private readonly logger: Logger; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); - } - public setup(core: CoreSetup) { - this.logger.debug('discover: Setup'); - core.capabilities.registerProvider(capabilitiesProvider); + core.uiSettings.register(uiSettings); return {}; } public start(core: CoreStart) { - this.logger.debug('discover: Started'); return {}; } diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts new file mode 100644 index 0000000000000..3eca11cc584a9 --- /dev/null +++ b/src/plugins/discover/server/ui_settings.ts @@ -0,0 +1,166 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { UiSettingsParams } from 'kibana/server'; +import { + DEFAULT_COLUMNS_SETTING, + SAMPLE_SIZE_SETTING, + AGGS_TERMS_SIZE_SETTING, + SORT_DEFAULT_ORDER_SETTING, + SEARCH_ON_PAGE_LOAD_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, + FIELDS_LIMIT_SETTING, + CONTEXT_DEFAULT_SIZE_SETTING, + CONTEXT_STEP_SETTING, + CONTEXT_TIE_BREAKER_FIELDS_SETTING, +} from '../common'; + +export const uiSettings: Record = { + [DEFAULT_COLUMNS_SETTING]: { + name: i18n.translate('discover.advancedSettings.defaultColumnsTitle', { + defaultMessage: 'Default columns', + }), + value: ['_source'], + description: i18n.translate('discover.advancedSettings.defaultColumnsText', { + defaultMessage: 'Columns displayed by default in the Discovery tab', + }), + category: ['discover'], + schema: schema.arrayOf(schema.string()), + }, + [SAMPLE_SIZE_SETTING]: { + name: i18n.translate('discover.advancedSettings.sampleSizeTitle', { + defaultMessage: 'Number of rows', + }), + value: 500, + description: i18n.translate('discover.advancedSettings.sampleSizeText', { + defaultMessage: 'The number of rows to show in the table', + }), + category: ['discover'], + schema: schema.number(), + }, + [AGGS_TERMS_SIZE_SETTING]: { + name: i18n.translate('discover.advancedSettings.aggsTermsSizeTitle', { + defaultMessage: 'Number of terms', + }), + value: 20, + type: 'number', + description: i18n.translate('discover.advancedSettings.aggsTermsSizeText', { + defaultMessage: + 'Determines how many terms will be visualized when clicking the "visualize" ' + + 'button, in the field drop downs, in the discover sidebar.', + }), + category: ['discover'], + schema: schema.number(), + }, + [SORT_DEFAULT_ORDER_SETTING]: { + name: i18n.translate('discover.advancedSettings.sortDefaultOrderTitle', { + defaultMessage: 'Default sort direction', + }), + value: 'desc', + options: ['desc', 'asc'], + optionLabels: { + desc: i18n.translate('discover.advancedSettings.sortOrderDesc', { + defaultMessage: 'Descending', + }), + asc: i18n.translate('discover.advancedSettings.sortOrderAsc', { + defaultMessage: 'Ascending', + }), + }, + type: 'select', + description: i18n.translate('discover.advancedSettings.sortDefaultOrderText', { + defaultMessage: + 'Controls the default sort direction for time based index patterns in the Discover app.', + }), + category: ['discover'], + schema: schema.oneOf([schema.literal('desc'), schema.literal('asc')]), + }, + [SEARCH_ON_PAGE_LOAD_SETTING]: { + name: i18n.translate('discover.advancedSettings.searchOnPageLoadTitle', { + defaultMessage: 'Search on page load', + }), + value: true, + type: 'boolean', + description: i18n.translate('discover.advancedSettings.searchOnPageLoadText', { + defaultMessage: + 'Controls whether a search is executed when Discover first loads. This setting does not ' + + 'have an effect when loading a saved search.', + }), + category: ['discover'], + schema: schema.boolean(), + }, + [DOC_HIDE_TIME_COLUMN_SETTING]: { + name: i18n.translate('discover.advancedSettings.docTableHideTimeColumnTitle', { + defaultMessage: "Hide 'Time' column", + }), + value: false, + description: i18n.translate('discover.advancedSettings.docTableHideTimeColumnText', { + defaultMessage: "Hide the 'Time' column in Discover and in all Saved Searches on Dashboards.", + }), + category: ['discover'], + schema: schema.boolean(), + }, + [FIELDS_LIMIT_SETTING]: { + name: i18n.translate('discover.advancedSettings.fieldsPopularLimitTitle', { + defaultMessage: 'Popular fields limit', + }), + value: 10, + description: i18n.translate('discover.advancedSettings.fieldsPopularLimitText', { + defaultMessage: 'The top N most popular fields to show', + }), + schema: schema.number(), + }, + [CONTEXT_DEFAULT_SIZE_SETTING]: { + name: i18n.translate('discover.advancedSettings.context.defaultSizeTitle', { + defaultMessage: 'Context size', + }), + value: 5, + description: i18n.translate('discover.advancedSettings.context.defaultSizeText', { + defaultMessage: 'The number of surrounding entries to show in the context view', + }), + category: ['discover'], + schema: schema.number(), + }, + [CONTEXT_STEP_SETTING]: { + name: i18n.translate('discover.advancedSettings.context.sizeStepTitle', { + defaultMessage: 'Context size step', + }), + value: 5, + description: i18n.translate('discover.advancedSettings.context.sizeStepText', { + defaultMessage: 'The step size to increment or decrement the context size by', + }), + category: ['discover'], + schema: schema.number(), + }, + [CONTEXT_TIE_BREAKER_FIELDS_SETTING]: { + name: i18n.translate('discover.advancedSettings.context.tieBreakerFieldsTitle', { + defaultMessage: 'Tie breaker fields', + }), + value: ['_doc'], + description: i18n.translate('discover.advancedSettings.context.tieBreakerFieldsText', { + defaultMessage: + 'A comma-separated list of fields to use for tie-breaking between documents that have the same timestamp value. ' + + 'From this list the first field that is present and sortable in the current index pattern is used.', + }), + category: ['discover'], + schema: schema.arrayOf(schema.string()), + }, +}; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index c097e3e8c13be..2b447c89e2850 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -27,7 +27,6 @@ export interface EmbeddableContext { export interface ValueClickTriggerContext { embeddable?: T; - timeFieldName?: string; data: { data: Array<{ table: Pick; @@ -35,6 +34,7 @@ export interface ValueClickTriggerContext { row: number; value: any; }>; + timeFieldName?: string; negate?: boolean; }; } @@ -45,11 +45,11 @@ export const isValueClickTriggerContext = ( export interface RangeSelectTriggerContext { embeddable?: T; - timeFieldName?: string; data: { table: KibanaDatatable; column: number; range: number[]; + timeFieldName?: string; }; } 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 4c2dac4f39134..58003a7e9e841 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 @@ -37,11 +37,9 @@ import { EuiCallOut, EuiBasicTableColumn, } from '@elastic/eui'; -import { ToastsStart, IUiSettingsClient } from 'kibana/public'; +import { ToastsStart } from 'kibana/public'; import { toMountPoint } from '../util'; -export const EMPTY_FILTER = ''; - interface Column { name: string; width?: string; @@ -61,12 +59,12 @@ export interface TableListViewProps { findItems(query: string): Promise<{ total: number; hits: object[] }>; listingLimit: number; initialFilter: string; + initialPageSize: number; noItemsFragment: JSX.Element; // update possible column types to something like (FieldDataColumn | ComputedColumn | ActionsColumn)[] when they have been added to EUI tableColumns: Column[]; tableListTitle: string; toastNotifications: ToastsStart; - uiSettings: IUiSettingsClient; /** * Id of the heading element describing the table. This id will be used as `aria-labelledby` of the wrapper element. * If the table is not empty, this component renders its own h1 element using the same id. @@ -98,11 +96,10 @@ class TableListView extends React.Component { type: string; name: string; @@ -131,7 +133,7 @@ class SavedObjectFinderUi extends React.Component< .map(metaData => metaData.includeFields || []) .reduce((allFields, currentFields) => allFields.concat(currentFields), ['title']); - const perPage = this.props.uiSettings.get('savedObjects:listingLimit'); + const perPage = this.props.uiSettings.get(LISTING_LIMIT_SETTING); const resp = await this.props.savedObjects.client.find({ type: Object.keys(metaDataMap), fields: [...new Set(fields)], diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index e38a0ef9830ea..4f7a4ff7f196f 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -36,5 +36,6 @@ export { isErrorNonFatal, } from './saved_object'; export { SavedObjectSaveOpts, SavedObjectKibanaServices, SavedObject } from './types'; +export { SavedObjectsStart } from './plugin'; export const plugin = () => new SavedObjectsPublicPlugin(); diff --git a/src/plugins/saved_objects/public/plugin.ts b/src/plugins/saved_objects/public/plugin.ts index 7927238e12066..d430c8896484d 100644 --- a/src/plugins/saved_objects/public/plugin.ts +++ b/src/plugins/saved_objects/public/plugin.ts @@ -22,9 +22,14 @@ import { CoreStart, Plugin } from 'src/core/public'; import './index.scss'; import { createSavedObjectClass } from './saved_object'; import { DataPublicPluginStart } from '../../data/public'; +import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; export interface SavedObjectsStart { SavedObjectClass: any; + settings: { + getPerPage: () => number; + getListingLimit: () => number; + }; } export interface SavedObjectsStartDeps { @@ -43,6 +48,10 @@ export class SavedObjectsPublicPlugin chrome: core.chrome, overlays: core.overlays, }), + settings: { + getPerPage: () => core.uiSettings.get(PER_PAGE_SETTING), + getListingLimit: () => core.uiSettings.get(LISTING_LIMIT_SETTING), + }, }; } } diff --git a/src/plugins/saved_objects/server/index.ts b/src/plugins/saved_objects/server/index.ts new file mode 100644 index 0000000000000..4e72bfef0637e --- /dev/null +++ b/src/plugins/saved_objects/server/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsServerPlugin } from './plugin'; + +export const plugin = () => new SavedObjectsServerPlugin(); diff --git a/src/plugins/saved_objects/server/plugin.ts b/src/plugins/saved_objects/server/plugin.ts new file mode 100644 index 0000000000000..9a656fcb510ac --- /dev/null +++ b/src/plugins/saved_objects/server/plugin.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, Plugin } from 'kibana/server'; +import { uiSettings } from './ui_settings'; + +export class SavedObjectsServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(uiSettings); + return {}; + } + + public start() { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/saved_objects/server/ui_settings.ts b/src/plugins/saved_objects/server/ui_settings.ts new file mode 100644 index 0000000000000..2e2e00b2c3ca7 --- /dev/null +++ b/src/plugins/saved_objects/server/ui_settings.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { UiSettingsParams } from 'kibana/server'; +import { PER_PAGE_SETTING, LISTING_LIMIT_SETTING } from '../common'; + +export const uiSettings: Record = { + [PER_PAGE_SETTING]: { + name: i18n.translate('savedObjects.advancedSettings.perPageTitle', { + defaultMessage: 'Objects per page', + }), + value: 20, + type: 'number', + description: i18n.translate('savedObjects.advancedSettings.perPageText', { + defaultMessage: 'Number of objects to show per page in the load dialog', + }), + schema: schema.number(), + }, + [LISTING_LIMIT_SETTING]: { + name: i18n.translate('savedObjects.advancedSettings.listingLimitTitle', { + defaultMessage: 'Objects listing limit', + }), + type: 'number', + value: 1000, + description: i18n.translate('savedObjects.advancedSettings.listingLimitText', { + defaultMessage: 'Number of objects to fetch for the listing pages', + }), + schema: schema.number(), + }, +}; diff --git a/src/plugins/vis_type_timeseries/common/constants.ts b/src/plugins/vis_type_timeseries/common/constants.ts new file mode 100644 index 0000000000000..fc402d6ab7db5 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/constants.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const MAX_BUCKETS_SETTING = 'metrics:max_buckets'; diff --git a/src/plugins/vis_type_timeseries/public/request_handler.js b/src/plugins/vis_type_timeseries/public/request_handler.js index bd6c6d9553930..e33d0e254f609 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.js +++ b/src/plugins/vis_type_timeseries/public/request_handler.js @@ -19,6 +19,7 @@ import { getTimezone, validateInterval } from './application'; import { getUISettings, getDataStart, getCoreStart } from './services'; +import { MAX_BUCKETS_SETTING } from '../common/constants'; export const metricsRequestHandler = async ({ uiState, @@ -37,7 +38,7 @@ export const metricsRequestHandler = async ({ if (visParams && visParams.id && !visParams.isModelInvalid) { try { - const maxBuckets = config.get('metrics:max_buckets'); + const maxBuckets = config.get(MAX_BUCKETS_SETTING); validateInterval(parsedTimeRange, visParams, maxBuckets); diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 05257cb79a75c..d863937a4e3dc 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -37,6 +37,7 @@ import { visDataRoutes } from './routes/vis'; // @ts-ignore import { fieldsRoutes } from './routes/fields'; import { SearchStrategyRegistry } from './lib/search_strategies'; +import { uiSettings } from './ui_settings'; export interface LegacySetup { server: Server; @@ -75,6 +76,7 @@ export class VisTypeTimeseriesPlugin implements Plugin { public setup(core: CoreSetup, plugins: VisTypeTimeseriesPluginSetupDependencies) { const logger = this.initializerContext.logger.get('visTypeTimeseries'); + core.uiSettings.register(uiSettings); const config$ = this.initializerContext.config.create(); // Global config contains things like the ES shard timeout const globalConfig$ = this.initializerContext.config.legacy.globalConfig$; diff --git a/src/plugins/vis_type_timeseries/server/ui_settings.ts b/src/plugins/vis_type_timeseries/server/ui_settings.ts new file mode 100644 index 0000000000000..65bc166a47104 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/ui_settings.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { UiSettingsParams } from 'kibana/server'; + +import { MAX_BUCKETS_SETTING } from '../common/constants'; + +export const uiSettings: Record = { + [MAX_BUCKETS_SETTING]: { + name: i18n.translate('visTypeTimeseries.advancedSettings.maxBucketsTitle', { + defaultMessage: 'Maximum buckets', + }), + value: 2000, + description: i18n.translate('visTypeTimeseries.advancedSettings.maxBucketsText', { + defaultMessage: 'The maximum number of buckets a single datasource can return', + }), + schema: schema.number(), + }, +}; diff --git a/src/plugins/vis_type_vislib/common/index.ts b/src/plugins/vis_type_vislib/common/index.ts new file mode 100644 index 0000000000000..e497253761a71 --- /dev/null +++ b/src/plugins/vis_type_vislib/common/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DIMMING_OPACITY_SETTING = 'visualization:dimmingOpacity'; +export const HEATMAP_MAX_BUCKETS_SETTING = 'visualization:heatmap:maxBuckets'; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js index b36ba336dbfe5..09256232860bc 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.js @@ -20,6 +20,7 @@ import d3 from 'd3'; import { get, pull, restParam, size, reduce } from 'lodash'; import $ from 'jquery'; +import { DIMMING_OPACITY_SETTING } from '../../../common'; /** * Handles event responses @@ -393,7 +394,7 @@ export class Dispatch { return function highlight(element) { const label = this.getAttribute('data-label'); if (!label) return; - const dimming = uiSettings.get('visualization:dimmingOpacity'); + const dimming = uiSettings.get(DIMMING_OPACITY_SETTING); $(element) .parent() .find('[data-label]') diff --git a/src/plugins/vis_type_vislib/public/vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.js index ca4f36f0ab0f7..f258cb55ba281 100644 --- a/src/plugins/vis_type_vislib/public/vislib/vis.js +++ b/src/plugins/vis_type_vislib/public/vislib/vis.js @@ -24,6 +24,7 @@ import { EventEmitter } from 'events'; import { VislibError } from './errors'; import { VisConfig } from './lib/vis_config'; import { Handler } from './lib/handler'; +import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../../common'; /** * Creates the visualizations. @@ -38,8 +39,8 @@ export class Vis extends EventEmitter { super(); this.element = element.get ? element.get(0) : element; this.visConfigArgs = _.cloneDeep(visConfigArgs); - this.visConfigArgs.dimmingOpacity = deps.uiSettings.get('visualization:dimmingOpacity'); - this.visConfigArgs.heatmapMaxBuckets = deps.uiSettings.get('visualization:heatmap:maxBuckets'); + this.visConfigArgs.dimmingOpacity = deps.uiSettings.get(DIMMING_OPACITY_SETTING); + this.visConfigArgs.heatmapMaxBuckets = deps.uiSettings.get(HEATMAP_MAX_BUCKETS_SETTING); this.deps = deps; } diff --git a/src/plugins/vis_type_vislib/server/index.ts b/src/plugins/vis_type_vislib/server/index.ts index 355c01d255ce7..c201dd3ddc49a 100644 --- a/src/plugins/vis_type_vislib/server/index.ts +++ b/src/plugins/vis_type_vislib/server/index.ts @@ -16,14 +16,10 @@ * specific language governing permissions and limitations * under the License. */ - import { schema } from '@kbn/config-schema'; - +import { VisTypeVislibServerPlugin } from './plugin'; export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), }; -export const plugin = () => ({ - setup() {}, - start() {}, -}); +export const plugin = () => new VisTypeVislibServerPlugin(); diff --git a/src/plugins/vis_type_vislib/server/plugin.ts b/src/plugins/vis_type_vislib/server/plugin.ts new file mode 100644 index 0000000000000..7ff81e070f7a5 --- /dev/null +++ b/src/plugins/vis_type_vislib/server/plugin.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup, CoreStart, Plugin } from 'kibana/server'; +import { uiSettings } from './ui_settings'; + +export class VisTypeVislibServerPlugin implements Plugin { + public setup(core: CoreSetup) { + core.uiSettings.register(uiSettings); + return {}; + } + + public start(core: CoreStart) { + return {}; + } + + public stop() {} +} diff --git a/src/plugins/vis_type_vislib/server/ui_settings.ts b/src/plugins/vis_type_vislib/server/ui_settings.ts new file mode 100644 index 0000000000000..a48cbbae3d0ca --- /dev/null +++ b/src/plugins/vis_type_vislib/server/ui_settings.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { UiSettingsParams } from 'kibana/server'; +import { DIMMING_OPACITY_SETTING, HEATMAP_MAX_BUCKETS_SETTING } from '../common'; + +export const uiSettings: Record = { + [DIMMING_OPACITY_SETTING]: { + name: i18n.translate('visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle', { + defaultMessage: 'Dimming opacity', + }), + value: 0.5, + type: 'number', + description: i18n.translate('visTypeVislib.advancedSettings.visualization.dimmingOpacityText', { + defaultMessage: + 'The opacity of the chart items that are dimmed when highlighting another element of the chart. ' + + 'The lower this number, the more the highlighted element will stand out. ' + + 'This must be a number between 0 and 1.', + }), + category: ['visualization'], + schema: schema.number(), + }, + [HEATMAP_MAX_BUCKETS_SETTING]: { + name: i18n.translate('visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle', { + defaultMessage: 'Heatmap maximum buckets', + }), + value: 50, + type: 'number', + description: i18n.translate( + 'visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText', + { + defaultMessage: + 'The maximum number of buckets a single datasource can return. ' + + 'A higher number might have negative impact on browser rendering performance', + } + ), + category: ['visualization'], + schema: schema.number(), + }, +}; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 9f0cfd7bf4d58..0306b943cbf2b 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -264,8 +264,7 @@ export class VisualizeEmbeddable extends Embeddable diff --git a/src/plugins/visualize/public/application/listing/visualize_listing.js b/src/plugins/visualize/public/application/listing/visualize_listing.js index 2a8c80f9352a5..7c95fd6a7f4b0 100644 --- a/src/plugins/visualize/public/application/listing/visualize_listing.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing.js @@ -44,6 +44,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor toastNotifications, visualizations, core: { docLinks, savedObjects, uiSettings, application }, + savedObjects: savedObjectsPublic, } = getServices(); chrome.docTitle.change( @@ -121,7 +122,7 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor this.fetchItems = filter => { const isLabsEnabled = uiSettings.get('visualize:enableLabs'); return savedVisualizations - .findListItems(filter, uiSettings.get('savedObjects:listingLimit')) + .findListItems(filter, savedObjectsPublic.settings.getListingLimit()) .then(result => { this.totalItems = result.total; @@ -154,7 +155,8 @@ export function VisualizeListingController($scope, createNewVis, kbnUrlStateStor }, ]); - this.listingLimit = uiSettings.get('savedObjects:listingLimit'); + this.listingLimit = savedObjectsPublic.settings.getListingLimit(); + this.initialPageSize = savedObjectsPublic.settings.getPerPage(); addHelpMenuToAppChrome(chrome, docLinks); diff --git a/src/plugins/visualize/public/application/listing/visualize_listing_table.js b/src/plugins/visualize/public/application/listing/visualize_listing_table.js index 3d49194c6778f..a1cc11c93cafc 100644 --- a/src/plugins/visualize/public/application/listing/visualize_listing_table.js +++ b/src/plugins/visualize/public/application/listing/visualize_listing_table.js @@ -45,6 +45,7 @@ class VisualizeListingTable extends Component { editItem={visualizeCapabilities.save ? this.props.editItem : null} tableColumns={this.getTableColumns()} listingLimit={this.props.listingLimit} + initialPageSize={this.props.initialPageSize} selectable={item => item.canDelete} initialFilter={''} noItemsFragment={this.getNoItemsMessage()} diff --git a/src/plugins/visualize/public/kibana_services.ts b/src/plugins/visualize/public/kibana_services.ts index 7642c6575f211..ace9e50d400c5 100644 --- a/src/plugins/visualize/public/kibana_services.ts +++ b/src/plugins/visualize/public/kibana_services.ts @@ -36,6 +36,7 @@ import { SavedVisualizations } from './application/types'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { DefaultEditorController } from '../../vis_default_editor/public'; import { DashboardStart } from '../../dashboard/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; export interface VisualizeKibanaServices { pluginInitializerContext: PluginInitializerContext; @@ -58,6 +59,7 @@ export interface VisualizeKibanaServices { DefaultVisualizationEditor: typeof DefaultEditorController; createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; scopedHistory: () => ScopedHistory; + savedObjects: SavedObjectsStart; } let services: VisualizeKibanaServices | null = null; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 45316fd6fad62..9d1e89a024b61 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -43,6 +43,7 @@ import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/publ import { DefaultEditorController } from '../../vis_default_editor/public'; import { DashboardStart } from '../../dashboard/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; +import { SavedObjectsStart } from '../../saved_objects/public'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -51,6 +52,7 @@ export interface VisualizePluginStartDependencies { visualizations: VisualizationsStart; dashboard: DashboardStart; kibanaLegacy: KibanaLegacyStart; + savedObjects: SavedObjectsStart; } export interface VisualizePluginSetupDependencies { @@ -135,6 +137,7 @@ export class VisualizePlugin pluginsStart.visualizations.__LEGACY.createVisEmbeddableFromObject, dashboard: pluginsStart.dashboard, scopedHistory: () => this.currentHistory!, + savedObjects: pluginsStart.savedObjects, }; setServices(deps); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 333aa93b7776d..e9cf8235ff3f4 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -28,7 +28,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); - describe('visual builder', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/43150 + describe.skip('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index aefb1e8ee1620..8c60bc8c5c2f8 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -34,7 +34,7 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo const find = getService('find'); const globalNav = getService('globalNav'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['shield']); + const PageObjects = getPageObjects(['login']); const defaultTryTimeout = config.get('timeouts.try'); const defaultFindTimeout = config.get('timeouts.find'); @@ -76,12 +76,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo if (loginPage && !wantedLoginPage) { log.debug('Found login page'); if (config.get('security.disableTestUser')) { - await PageObjects.shield.login( + await PageObjects.login.login( config.get('servers.kibana.username'), config.get('servers.kibana.password') ); } else { - await PageObjects.shield.login('test_user', 'changeme'); + await PageObjects.login.login('test_user', 'changeme'); } await find.byCssSelector( diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 01301109b80ef..10b09c742f58e 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -28,7 +28,7 @@ import { HomePageProvider } from './home_page'; import { NewsfeedPageProvider } from './newsfeed_page'; import { SettingsPageProvider } from './settings_page'; import { SharePageProvider } from './share_page'; -import { ShieldPageProvider } from './shield_page'; +import { LoginPageProvider } from './login_page'; import { TimePickerProvider } from './time_picker'; import { TimelionPageProvider } from './timelion_page'; import { VisualBuilderPageProvider } from './visual_builder_page'; @@ -51,7 +51,7 @@ export const pageObjects = { newsfeed: NewsfeedPageProvider, settings: SettingsPageProvider, share: SharePageProvider, - shield: ShieldPageProvider, + login: LoginPageProvider, timelion: TimelionPageProvider, timePicker: TimePickerProvider, visualBuilder: VisualBuilderPageProvider, diff --git a/test/functional/page_objects/shield_page.ts b/test/functional/page_objects/login_page.ts similarity index 90% rename from test/functional/page_objects/shield_page.ts rename to test/functional/page_objects/login_page.ts index 2b9c59373a8bc..c84f47a342155 100644 --- a/test/functional/page_objects/shield_page.ts +++ b/test/functional/page_objects/login_page.ts @@ -19,10 +19,10 @@ import { FtrProviderContext } from '../ftr_provider_context'; -export function ShieldPageProvider({ getService }: FtrProviderContext) { +export function LoginPageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); - class ShieldPage { + class LoginPage { async login(user: string, pwd: string) { await testSubjects.setValue('loginUsername', user); await testSubjects.setValue('loginPassword', pwd); @@ -30,5 +30,5 @@ export function ShieldPageProvider({ getService }: FtrProviderContext) { } } - return new ShieldPage(); + return new LoginPage(); } diff --git a/x-pack/legacy/plugins/maps/public/routes.js b/x-pack/legacy/plugins/maps/public/routes.js index 70c1c4a50efd4..721b30a208ebc 100644 --- a/x-pack/legacy/plugins/maps/public/routes.js +++ b/x-pack/legacy/plugins/maps/public/routes.js @@ -18,6 +18,7 @@ import { } from '../../../../plugins/maps/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { getMapsSavedObjectLoader } from '../../../../plugins/maps/public/angular/services/gis_map_saved_object_loader'; +import { LISTING_LIMIT_SETTING } from '../../../../../src/plugins/saved_objects/common'; routes.enable(); @@ -43,7 +44,7 @@ routes template: listingTemplate, controller($scope, config) { const gisMapSavedObjectLoader = getMapsSavedObjectLoader(); - $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.listingLimit = config.get(LISTING_LIMIT_SETTING); $scope.find = search => { return gisMapSavedObjectLoader.find(search, $scope.listingLimit); }; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx index 2633771c8b031..5d1ca923cbc8f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Cytoscape.tsx @@ -146,7 +146,7 @@ export function Cytoscape({ }; const dataHandler: cytoscape.EventHandler = event => { - if (cy) { + if (cy && cy.elements().length > 0) { if (serviceName) { resetConnectedEdgeStyle(cy.getElementById(serviceName)); // Add the "primary" class to the node if its id matches the serviceName. diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index d3c4654de8164..3ec13a4cde20d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -58,9 +58,9 @@ const getBorderWidth = (el: cytoscape.NodeSingular) => { if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { return 4; } else if (nodeSeverity === severity.critical) { - return 12; + return 8; } else { - return 2; + return 4; } }; diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 9c2aa821be2d5..d97e4944da13a 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -27,9 +27,6 @@ import { getDocumentationLinks } from './lib/documentation_links'; import { HelpMenu } from './components/help_menu/help_menu'; import { createStore, destroyStore } from './store'; -import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../src/plugins/ui_actions/public'; -/* eslint-disable */ -import { ACTION_VALUE_CLICK } from '../../../../src/plugins/data/public/actions/value_click_action'; /* eslint-enable */ import { init as initStatsReporter } from './lib/ui_metric'; @@ -45,16 +42,6 @@ import './style/index.scss'; const { ReadOnlyBadge: strings } = CapabilitiesStrings; -let restoreAction: ActionByType | undefined; -const emptyAction = { - id: 'empty-action', - type: '', - getDisplayName: () => 'empty action', - getIconType: () => undefined, - isCompatible: async () => true, - execute: async () => undefined, -} as ActionByType; - export const renderApp = ( coreStart: CoreStart, plugins: CanvasStartDeps, @@ -134,17 +121,6 @@ export const initializeCanvas = async ( }, }); - // TODO: We need this to disable the filtering modal from popping up in lens embeds until - // they honor the disableTriggers parameter - const action = startPlugins.uiActions.getAction(ACTION_VALUE_CLICK); - - if (action) { - restoreAction = action; - - startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, action.id); - startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, emptyAction); - } - if (setupPlugins.usageCollection) { initStatsReporter(setupPlugins.usageCollection.reportUiStats); } @@ -158,12 +134,6 @@ export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDe resetInterpreter(); destroyStore(); - startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, emptyAction.id); - if (restoreAction) { - startPlugins.uiActions.addTriggerAction(VALUE_CLICK_TRIGGER, restoreAction); - restoreAction = undefined; - } - coreStart.chrome.setBadge(undefined); coreStart.chrome.setHelpExtension(undefined); diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index 846013674986e..4aa6725159043 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -27,7 +27,7 @@ export const getActions = (): FindActionResult[] => [ referencedByCount: 0, }, { - id: 'd611af27-3532-4da9-8034-271fee81d634', + id: '123', actionTypeId: '.servicenow', name: 'ServiceNow', config: { diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 5be36d34549b7..871e78495c5dd 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -37,15 +37,23 @@ export function initPushCaseUserActionApi({ async (context, request, response) => { try { const client = context.core.savedObjects.client; + const actionsClient = await context.actions?.getActionsClient(); + const caseId = request.params.case_id; const query = pipe( CaseExternalServiceRequestRt.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); + + if (actionsClient == null) { + throw Boom.notFound('Action client have not been found'); + } + const { username, full_name, email } = await caseService.getUser({ request, response }); + const pushedDate = new Date().toISOString(); - const [myCase, myCaseConfigure, totalCommentsFindByCases] = await Promise.all([ + const [myCase, myCaseConfigure, totalCommentsFindByCases, connectors] = await Promise.all([ caseService.getCase({ client, caseId: request.params.case_id, @@ -60,6 +68,7 @@ export function initPushCaseUserActionApi({ perPage: 1, }, }), + actionsClient.getAll(), ]); if (myCase.attributes.status === 'closed') { @@ -85,9 +94,15 @@ export function initPushCaseUserActionApi({ }; const caseConfigureConnectorId = getConnectorId(myCaseConfigure); + // old case may not have new attribute connector_id, so we default to the configured system - const updateConnectorId = - myCase.attributes.connector_id == null ? { connector_id: caseConfigureConnectorId } : {}; + const updateConnectorId = { + connector_id: myCase.attributes.connector_id ?? caseConfigureConnectorId, + }; + + if (!connectors.some(connector => connector.id === updateConnectorId.connector_id)) { + throw Boom.notFound('Connector not found or set to none'); + } const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 190400e988634..0a7eaf647b020 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -61,6 +61,50 @@ describe('', () => { }); }); + describe('when there are multiple pages of auto-follow patterns', () => { + let find; + let component; + let table; + let actions; + let form; + + const autoFollowPatterns = [ + getAutoFollowPatternMock({ name: 'unique', followPattern: '{{leader_index}}' }), + ]; + + for (let i = 0; i < 29; i++) { + autoFollowPatterns.push( + getAutoFollowPatternMock({ name: `${i}`, followPattern: '{{leader_index}}' }) + ); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadAutoFollowPatternsResponse({ patterns: autoFollowPatterns }); + + // Mount the component + ({ find, component, table, actions, form } = setup()); + + await nextTick(); // Make sure that the http request is fulfilled + component.update(); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('autoFollowPatternListTable'); + + // Pagination defaults to 20 auto-follow patterns per page. We loaded 30 auto-follow patterns, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('autoFollowPatternSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('autoFollowPatternListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are auto-follow patterns', () => { let find; let exists; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index f98a1dafbbcbf..ad9f2db2ce91c 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -4,6 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * The below import is required to avoid a console error warn from brace package + * console.warn ../node_modules/brace/index.js:3999 + Could not load worker ReferenceError: Worker is not defined + at createWorker (//node_modules/brace/index.js:17992:5) + */ +import * as stubWebWorker from '../../../../../test_utils/stub_web_worker'; // eslint-disable-line no-unused-vars + import { getFollowerIndexMock } from './fixtures/follower_index'; import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; @@ -59,6 +67,54 @@ describe('', () => { }); }); + describe('when there are multiple pages of follower indices', () => { + let find; + let component; + let table; + let actions; + let form; + + const followerIndices = [ + { + name: 'unique', + seeds: [], + }, + ]; + + for (let i = 0; i < 29; i++) { + followerIndices.push({ + name: `name${i}`, + seeds: [], + }); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadFollowerIndicesResponse({ indices: followerIndices }); + + // Mount the component + ({ find, component, table, actions, form } = setup()); + + await nextTick(); // Make sure that the http request is fulfilled + component.update(); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('followerIndexListTable'); + + // Pagination defaults to 20 follower indices per page. We loaded 30 follower indices, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('followerIndexSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('followerIndexListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are follower indices', () => { let find; let exists; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 450feed49f9f2..2c2ab642e83c8 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -84,6 +84,10 @@ export const setup = props => { autoFollowPatternLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('autoFollowPatternListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -94,6 +98,7 @@ export const setup = props => { clickAutoFollowPatternAt, getPatternsActionMenuItemText, clickPatternsActionMenuItem, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 52f4267594cc1..5e9f7d1263cf7 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -64,6 +64,10 @@ export const setup = props => { followerIndexLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('followerIndexListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -72,6 +76,7 @@ export const setup = props => { clickContextMenuButtonAt, openTableRowContextMenuAt, clickFollowerIndexAt, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index eb90e59e99fee..d682fdaadf818 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -23,6 +23,30 @@ import { import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; +const getFilteredPatterns = (autoFollowPatterns, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return autoFollowPatterns.filter(autoFollowPattern => { + const { + name, + remoteCluster, + followIndexPatternPrefix, + followIndexPatternSuffix, + } = autoFollowPattern; + + const inName = name.toLowerCase().includes(normalizedSearchText); + const inRemoteCluster = remoteCluster.toLowerCase().includes(normalizedSearchText); + const inPrefix = followIndexPatternPrefix.toLowerCase().includes(normalizedSearchText); + const inSuffix = followIndexPatternSuffix.toLowerCase().includes(normalizedSearchText); + + return inName || inRemoteCluster || inPrefix || inSuffix; + }); + } + + return autoFollowPatterns; +}; + export class AutoFollowPatternTable extends PureComponent { static propTypes = { autoFollowPatterns: PropTypes.array, @@ -31,41 +55,42 @@ export class AutoFollowPatternTable extends PureComponent { resumeAutoFollowPattern: PropTypes.func.isRequired, }; - state = { - selectedItems: [], - }; + static getDerivedStateFromProps(props, state) { + const { autoFollowPatterns } = props; + const { prevAutoFollowPatterns, queryText } = state; - onSearch = ({ query }) => { - const { text } = query; - const normalizedSearchText = text.toLowerCase(); - this.setState({ - queryText: normalizedSearchText, - }); - }; + // If an auto-follow pattern gets deleted, we need to recreate the cached filtered auto-follow patterns. + if (prevAutoFollowPatterns !== autoFollowPatterns) { + return { + prevAutoFollowPatterns: autoFollowPatterns, + filteredAutoFollowPatterns: getFilteredPatterns(autoFollowPatterns, queryText), + }; + } - getFilteredPatterns = () => { - const { autoFollowPatterns } = this.props; - const { queryText } = this.state; + return null; + } - if (queryText) { - return autoFollowPatterns.filter(autoFollowPattern => { - const { - name, - remoteCluster, - followIndexPatternPrefix, - followIndexPatternSuffix, - } = autoFollowPattern; + constructor(props) { + super(props); - const inName = name.toLowerCase().includes(queryText); - const inRemoteCluster = remoteCluster.toLowerCase().includes(queryText); - const inPrefix = followIndexPatternPrefix.toLowerCase().includes(queryText); - const inSuffix = followIndexPatternSuffix.toLowerCase().includes(queryText); + this.state = { + prevAutoFollowPatterns: props.autoFollowPatterns, + selectedItems: [], + filteredAutoFollowPatterns: props.autoFollowPatterns, + queryText: '', + }; + } - return inName || inRemoteCluster || inPrefix || inSuffix; - }); - } + onSearch = ({ query }) => { + const { autoFollowPatterns } = this.props; + const { text } = query; - return autoFollowPatterns.slice(0); + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. + this.setState({ + queryText: text, + filteredAutoFollowPatterns: getFilteredPatterns(autoFollowPatterns, text), + }); }; getTableColumns() { @@ -144,7 +169,7 @@ export class AutoFollowPatternTable extends PureComponent { defaultMessage: 'Leader patterns', } ), - render: leaderPatterns => leaderPatterns.join(', '), + render: leaderIndexPatterns => leaderIndexPatterns.join(', '), }, { field: 'followIndexPatternPrefix', @@ -278,7 +303,7 @@ export class AutoFollowPatternTable extends PureComponent { }; render() { - const { selectedItems } = this.state; + const { selectedItems, filteredAutoFollowPatterns } = this.state; const sorting = { sort: { @@ -297,13 +322,13 @@ export class AutoFollowPatternTable extends PureComponent { this.setState({ selectedItems: selectedItems.map(({ name }) => name) }), }; - const items = this.getFilteredPatterns(); - const search = { toolsLeft: selectedItems.length ? ( items.find(item => item.name === name))} + patterns={this.state.selectedItems.map(name => + filteredAutoFollowPatterns.find(item => item.name === name) + )} /> ) : ( undefined @@ -311,13 +336,14 @@ export class AutoFollowPatternTable extends PureComponent { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'autoFollowPatternSearch', }, }; return ( ({ apiStatusDelete: getApiStatus(`${scope}-delete`)(state), }); -// + const mapDispatchToProps = dispatch => ({ selectFollowerIndex: name => dispatch(selectDetailFollowerIndex(name)), }); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js index ef4a511f276bd..e95b3b0356aba 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js @@ -26,21 +26,73 @@ import { routing } from '../../../../../services/routing'; import { trackUiMetric } from '../../../../../services/track_ui_metric'; import { ContextMenu } from '../context_menu'; +const getFilteredIndices = (followerIndices, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return followerIndices.filter(followerIndex => { + const { name, remoteCluster, leaderIndex } = followerIndex; + + if (name.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + if (leaderIndex.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + if (remoteCluster.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + return false; + }); + } + + return followerIndices; +}; + export class FollowerIndicesTable extends PureComponent { static propTypes = { followerIndices: PropTypes.array, selectFollowerIndex: PropTypes.func.isRequired, }; - state = { - selectedItems: [], - }; + static getDerivedStateFromProps(props, state) { + const { followerIndices } = props; + const { prevFollowerIndices, queryText } = state; + + // If a follower index gets deleted, we need to recreate the cached filtered follower indices. + if (prevFollowerIndices !== followerIndices) { + return { + prevFollowerIndices: followerIndices, + filteredClusters: getFilteredIndices(followerIndices, queryText), + }; + } + + return null; + } + + constructor(props) { + super(props); + + this.state = { + prevFollowerIndices: props.followerIndices, + selectedItems: [], + filteredIndices: props.followerIndices, + queryText: '', + }; + } onSearch = ({ query }) => { + const { followerIndices } = this.props; const { text } = query; - const normalizedSearchText = text.toLowerCase(); + + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. this.setState({ - queryText: normalizedSearchText, + queryText: text, + filteredIndices: getFilteredIndices(followerIndices, text), }); }; @@ -49,25 +101,6 @@ export class FollowerIndicesTable extends PureComponent { routing.navigate(uri); }; - getFilteredIndices = () => { - const { followerIndices } = this.props; - const { queryText } = this.state; - - if (queryText) { - return followerIndices.filter(followerIndex => { - const { name, remoteCluster, leaderIndex } = followerIndex; - - const inName = name.toLowerCase().includes(queryText); - const inRemoteCluster = remoteCluster.toLowerCase().includes(queryText); - const inLeaderIndex = leaderIndex.toLowerCase().includes(queryText); - - return inName || inRemoteCluster || inLeaderIndex; - }); - } - - return followerIndices.slice(0); - }; - getTableColumns() { const { selectFollowerIndex } = this.props; @@ -258,7 +291,7 @@ export class FollowerIndicesTable extends PureComponent { }; render() { - const { selectedItems } = this.state; + const { selectedItems, filteredIndices } = this.state; const sorting = { sort: { @@ -285,13 +318,14 @@ export class FollowerIndicesTable extends PureComponent { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'followerIndexSearch', }, }; return ( -1; } diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx index 1ead7a38d4c9b..c94d19d28e6da 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.test.tsx @@ -134,10 +134,12 @@ describe('.execute() & getHref', () => { }; const context = ({ - data: useRangeEvent - ? ({ range: {} } as RangeSelectTriggerContext['data']) - : ({ data: [] } as ValueClickTriggerContext['data']), - timeFieldName: 'order_date', + data: { + ...(useRangeEvent + ? ({ range: {} } as RangeSelectTriggerContext['data']) + : ({ data: [] } as ValueClickTriggerContext['data'])), + timeFieldName: 'order_date', + }, embeddable: { getInput: () => ({ filters: [], diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx index d33dd1ef64e0d..21afa6e822dc5 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/dashboard_to_dashboard_drilldown/drilldown.tsx @@ -127,9 +127,9 @@ export class DashboardToDashboardDrilldown } })(); - if (context.timeFieldName) { + if (context.data.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - context.timeFieldName, + context.data.timeFieldName, filtersFromEvent ); filtersFromEvent = restOfFilters; diff --git a/x-pack/plugins/graph/common/constants.ts b/x-pack/plugins/graph/common/constants.ts index 3c3ee2b125896..42c05cfde1a44 100644 --- a/x-pack/plugins/graph/common/constants.ts +++ b/x-pack/plugins/graph/common/constants.ts @@ -7,5 +7,5 @@ export const APP_ICON = 'graphApp'; export function createWorkspacePath(id: string) { - return `/app/graph/#/workspace/${id}`; + return `/app/graph#/workspace/${id}`; } diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index 96e7ea3ff2232..4cae14f8939b2 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["licensing", "data", "navigation"], + "requiredPlugins": ["licensing", "data", "navigation", "savedObjects"], "optionalPlugins": ["home", "features"], "configPath": ["xpack", "graph"] } diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html index 2f75d663126b2..4e0c13442d267 100644 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html @@ -7,5 +7,6 @@ listing-limit="listingLimit" capabilities="capabilities" initial-filter="initialFilter" + initialPageSize="initialPageSize" core-start="coreStart" > diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 7effe44375b1f..d4199fbd092b4 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -46,13 +46,13 @@ export function initGraphApp(angularModule, deps) { addBasePath, getBasePath, data, - config, capabilities, coreStart, storage, canEditDrillDownUrls, graphSavePolicy, overlays, + savedObjects, } = deps; const app = angularModule; @@ -77,6 +77,7 @@ export function initGraphApp(angularModule, deps) { ['hideWriteControls', { watchDepth: 'reference' }], ['capabilities', { watchDepth: 'reference' }], ['initialFilter', { watchDepth: 'reference' }], + ['initialPageSize', { watchDepth: 'reference' }], ]); }); @@ -111,7 +112,8 @@ export function initGraphApp(angularModule, deps) { template: listingTemplate, badge: getReadonlyBadge, controller: function($location, $scope) { - $scope.listingLimit = config.get('savedObjects:listingLimit'); + $scope.listingLimit = savedObjects.settings.getListingLimit(); + $scope.initialPageSize = savedObjects.settings.getPerPage(); $scope.create = () => { $location.url(getNewPath()); }; diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 4aff9d28f3771..7c0fb867b9ada 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -22,7 +22,6 @@ import { PluginInitializerContext, SavedObjectsClientContract, ToastsStart, - IUiSettingsClient, OverlayStart, } from 'kibana/public'; // @ts-ignore @@ -39,6 +38,7 @@ import { } from '../../../../src/plugins/kibana_legacy/public'; import './index.scss'; +import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; /** * These are dependencies of the Graph app besides the base dependencies @@ -56,7 +56,6 @@ export interface GraphDependencies { navigation: NavigationStart; licensing: LicensingPluginSetup; chrome: ChromeStart; - config: IUiSettingsClient; toastNotifications: ToastsStart; indexPatterns: IndexPatternsContract; data: ReturnType; @@ -67,6 +66,7 @@ export interface GraphDependencies { canEditDrillDownUrls: boolean; graphSavePolicy: string; overlays: OverlayStart; + savedObjects: SavedObjectsStart; } export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) => { diff --git a/x-pack/plugins/graph/public/components/listing.tsx b/x-pack/plugins/graph/public/components/listing.tsx index 37f27523cbad2..b89ee2489d7f3 100644 --- a/x-pack/plugins/graph/public/components/listing.tsx +++ b/x-pack/plugins/graph/public/components/listing.tsx @@ -24,6 +24,7 @@ export interface ListingProps { hideWriteControls: boolean; capabilities: { save: boolean; delete: boolean }; initialFilter: string; + initialPageSize: number; } export function Listing(props: ListingProps) { @@ -38,6 +39,7 @@ export function Listing(props: ListingProps) { tableColumns={getTableColumns(props.getViewUrl)} listingLimit={props.listingLimit} initialFilter={props.initialFilter} + initialPageSize={props.initialPageSize} noItemsFragment={getNoItemsMessage( props.capabilities.save === false, props.createItem, @@ -53,7 +55,6 @@ export function Listing(props: ListingProps) { tableListTitle={i18n.translate('xpack.graph.listing.graphsTitle', { defaultMessage: 'Graphs', })} - uiSettings={props.coreStart.uiSettings} /> ); diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index b869119062538..9d6d083b3e4b9 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -23,6 +23,7 @@ import { } from '../../../../src/plugins/home/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { ConfigSchema } from '../config'; +import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; export interface GraphPluginSetupDependencies { licensing: LicensingPluginSetup; @@ -32,6 +33,7 @@ export interface GraphPluginSetupDependencies { export interface GraphPluginStartDependencies { navigation: NavigationStart; data: DataPublicPluginStart; + savedObjects: SavedObjectsStart; } export class GraphPlugin @@ -92,10 +94,10 @@ export class GraphPlugin capabilities: coreStart.application.capabilities.graph, coreStart, chrome: coreStart.chrome, - config: coreStart.uiSettings, toastNotifications: coreStart.notifications.toasts, indexPatterns: pluginsStart.data!.indexPatterns, overlays: coreStart.overlays, + savedObjects: pluginsStart.savedObjects, }); }, }); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js index 5d0e2fd4530ef..f6250a7a6e25c 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.container.js @@ -11,7 +11,7 @@ import { getPager, getFilter, isDetailPanelOpen, - showSystemIndices, + showHiddenIndices, getSortField, isSortAscending, getIndicesAsArray, @@ -26,7 +26,7 @@ import { pageChanged, pageSizeChanged, sortChanged, - showSystemIndicesChanged, + showHiddenIndicesChanged, loadIndices, reloadIndices, toggleChanged, @@ -42,7 +42,7 @@ const mapStateToProps = state => { indices: getPageOfIndices(state), pager: getPager(state), filter: getFilter(state), - showSystemIndices: showSystemIndices(state), + showHiddenIndices: showHiddenIndices(state), sortField: getSortField(state), isSortAscending: isSortAscending(state), indicesLoading: indicesLoading(state), @@ -65,8 +65,8 @@ const mapDispatchToProps = dispatch => { sortChanged: (sortField, isSortAscending) => { dispatch(sortChanged({ sortField, isSortAscending })); }, - showSystemIndicesChanged: showSystemIndices => { - dispatch(showSystemIndicesChanged({ showSystemIndices })); + showHiddenIndicesChanged: showHiddenIndices => { + dispatch(showHiddenIndicesChanged({ showHiddenIndices })); }, toggleChanged: (toggleName, toggleValue) => { dispatch(toggleChanged({ toggleName, toggleValue })); diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js index b659e2ec86a9b..7a0a6269a6ab8 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_table/index_table.js @@ -400,8 +400,8 @@ export class IndexTable extends Component { render() { const { filter, - showSystemIndices, - showSystemIndicesChanged, + showHiddenIndices, + showHiddenIndicesChanged, indices, loadIndices, indicesLoading, @@ -459,13 +459,13 @@ export class IndexTable extends Component { })} showSystemIndicesChanged(event.target.checked)} + id="checkboxShowHiddenIndices" + checked={showHiddenIndices} + onChange={event => showHiddenIndicesChanged(event.target.checked)} label={ } /> diff --git a/x-pack/plugins/index_management/public/application/store/actions/table_state.js b/x-pack/plugins/index_management/public/application/store/actions/table_state.js index cc6c66aab4f1a..70e0de74d0278 100644 --- a/x-pack/plugins/index_management/public/application/store/actions/table_state.js +++ b/x-pack/plugins/index_management/public/application/store/actions/table_state.js @@ -17,8 +17,8 @@ export const pageSizeChanged = createAction('INDEX_MANAGEMENT_PAGE_SIZE_CHANGED' export const sortChanged = createAction('INDEX_MANAGEMENT_SORT_CHANGED'); -export const showSystemIndicesChanged = createAction( - 'INDEX_MANAGEMENT_SHOW_SYSTEM_INDICES_CHANGED' +export const showHiddenIndicesChanged = createAction( + 'INDEX_MANAGEMENT_SHOW_HIDDEN_INDICES_CHANGED' ); export const toggleChanged = createAction('INDEX_MANAGEMENT_TOGGLE_CHANGED'); diff --git a/x-pack/plugins/index_management/public/application/store/reducers/table_state.js b/x-pack/plugins/index_management/public/application/store/reducers/table_state.js index 8bb4e77619ef6..e90fa72aa62fe 100644 --- a/x-pack/plugins/index_management/public/application/store/reducers/table_state.js +++ b/x-pack/plugins/index_management/public/application/store/reducers/table_state.js @@ -10,7 +10,7 @@ import { pageChanged, pageSizeChanged, sortChanged, - showSystemIndicesChanged, + showHiddenIndicesChanged, toggleChanged, } from '../actions'; @@ -20,7 +20,7 @@ export const defaultTableState = { currentPage: 0, sortField: 'index.name', isSortAscending: true, - showSystemIndices: false, + showHiddenIndices: false, }; export const tableState = handleActions( @@ -33,12 +33,12 @@ export const tableState = handleActions( currentPage: 0, }; }, - [showSystemIndicesChanged](state, action) { - const { showSystemIndices } = action.payload; + [showHiddenIndicesChanged](state, action) { + const { showHiddenIndices } = action.payload; return { ...state, - showSystemIndices, + showHiddenIndices, }; }, [toggleChanged](state, action) { diff --git a/x-pack/plugins/index_management/public/application/store/selectors/index.js b/x-pack/plugins/index_management/public/application/store/selectors/index.js index 3ddbf28651fa0..0f481c55782b0 100644 --- a/x-pack/plugins/index_management/public/application/store/selectors/index.js +++ b/x-pack/plugins/index_management/public/application/store/selectors/index.js @@ -86,9 +86,9 @@ const getFilteredIndices = createSelector( (indices, allIds, tableState) => { let indexArray = allIds.map(indexName => indices[indexName]); indexArray = filterByToggles(indexArray, tableState.toggleNameToVisibleMap); - const systemFilteredIndexes = tableState.showSystemIndices + const systemFilteredIndexes = tableState.showHiddenIndices ? indexArray - : indexArray.filter(index => !(index.name + '').startsWith('.')); + : indexArray.filter(index => !(index.name + '').startsWith('.') && !index.hidden); const filter = tableState.filter || EuiSearchBar.Query.MATCH_ALL; return EuiSearchBar.Query.execute(filter, systemFilteredIndexes, { defaultFields: defaultFilterFields, @@ -151,9 +151,9 @@ export const getCurrentPage = createSelector(getPager, pager => { export const getFilter = createSelector(getTableState, ({ filter }) => filter); -export const showSystemIndices = createSelector( +export const showHiddenIndices = createSelector( getTableState, - ({ showSystemIndices }) => showSystemIndices + ({ showHiddenIndices }) => showHiddenIndices ); export const isSortAscending = createSelector( diff --git a/x-pack/plugins/index_management/server/lib/fetch_aliases.test.ts b/x-pack/plugins/index_management/server/lib/fetch_aliases.test.ts deleted file mode 100644 index d5ba11df1f0a9..0000000000000 --- a/x-pack/plugins/index_management/server/lib/fetch_aliases.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import sinon from 'sinon'; -import { fetchAliases } from './fetch_aliases'; - -describe('fetching aliases', () => { - const fetchFn = fetchAliases; - - test('should return map of aliases for indices', async () => { - const retVal = [ - { index: 'test1Index', alias: 'test1Alias' }, - { index: 'test2Index', alias: 'test1Alias' }, - { index: 'test3Index', alias: 'test2Alias' }, - { index: 'test3Index', alias: 'test3Alias' }, - ]; - const mockCallWithRequest = sinon.spy(() => { - return retVal; - }); - - const results = await fetchFn(mockCallWithRequest); - - expect(mockCallWithRequest.called); - expect(results).toBeDefined(); - expect(results).toMatchObject({ - test1Index: ['test1Alias'], - test2Index: ['test1Alias'], - test3Index: ['test2Alias', 'test3Alias'], - }); - }); - - test('should return an empty object if no aliases exist', async () => { - const mockCallWithRequest = sinon.spy(() => { - return []; - }); - - const results = await fetchFn(mockCallWithRequest); - - expect(mockCallWithRequest.called); - expect(results).toBeDefined(); - expect(results).toMatchObject({}); - }); -}); diff --git a/x-pack/plugins/index_management/server/lib/fetch_aliases.ts b/x-pack/plugins/index_management/server/lib/fetch_aliases.ts deleted file mode 100644 index 794a260fb9c51..0000000000000 --- a/x-pack/plugins/index_management/server/lib/fetch_aliases.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export async function fetchAliases(callWithRequest: any) { - const results = await callWithRequest('cat.aliases', { format: 'json' }); - return results.reduce( - (hash: { [key: string]: any }, { index, alias }: { index: string; alias: string }) => { - (hash[index] = hash[index] || []).push(alias); - return hash; - }, - {} - ); -} diff --git a/x-pack/plugins/index_management/server/lib/fetch_indices.ts b/x-pack/plugins/index_management/server/lib/fetch_indices.ts index d9f01ee060145..1f62680a41cbc 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { CatIndicesParams } from 'elasticsearch'; import { IndexDataEnricher } from '../services'; import { Index, CallAsCurrentUser } from '../types'; -import { fetchAliases } from './fetch_aliases'; interface Hit { health: string; @@ -17,20 +17,64 @@ interface Hit { 'docs.count': any; 'store.size': any; sth: 'true' | 'false'; + hidden: boolean; } -interface Aliases { - [key: string]: string[]; +interface IndexInfo { + aliases: { [aliasName: string]: unknown }; + mappings: unknown; + settings: { + index: { + hidden: 'true' | 'false'; + }; + }; } -interface Params { - format: string; - h: string; - index?: string[]; +interface GetIndicesResponse { + [indexName: string]: IndexInfo; } -function formatHits(hits: Hit[], aliases: Aliases): Index[] { - return hits.map((hit: Hit) => { +async function fetchIndicesCall( + callAsCurrentUser: CallAsCurrentUser, + indexNames?: string[] +): Promise { + const indexNamesString = indexNames && indexNames.length ? indexNames.join(',') : '*'; + + // This call retrieves alias and settings (incl. hidden status) information about indices + const indices: GetIndicesResponse = await callAsCurrentUser('transport.request', { + method: 'GET', + path: `/${indexNamesString}`, + query: { + expand_wildcards: 'hidden,all', + }, + }); + + if (!Object.keys(indices).length) { + return []; + } + + const catQuery: Pick & { + expand_wildcards: string; + index?: string; + } = { + format: 'json', + h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size', + expand_wildcards: 'hidden,all', + index: indexNamesString, + }; + + // This call retrieves health and other high-level information about indices. + const catHits: Hit[] = await callAsCurrentUser('transport.request', { + method: 'GET', + path: '/_cat/indices', + query: catQuery, + }); + + // The two responses should be equal in the number of indices returned + return catHits.map(hit => { + const index = indices[hit.index]; + const aliases = Object.keys(index.aliases); + return { health: hit.health, status: hit.status, @@ -41,32 +85,17 @@ function formatHits(hits: Hit[], aliases: Aliases): Index[] { documents: hit['docs.count'], size: hit['store.size'], isFrozen: hit.sth === 'true', // sth value coming back as a string from ES - aliases: aliases.hasOwnProperty(hit.index) ? aliases[hit.index] : 'none', + aliases: aliases.length ? aliases : 'none', + hidden: index.settings.index.hidden === 'true', }; }); } -async function fetchIndicesCall(callAsCurrentUser: CallAsCurrentUser, indexNames?: string[]) { - const params: Params = { - format: 'json', - h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size', - }; - - if (indexNames) { - params.index = indexNames; - } - - return await callAsCurrentUser('cat.indices', params); -} - export const fetchIndices = async ( callAsCurrentUser: CallAsCurrentUser, indexDataEnricher: IndexDataEnricher, indexNames?: string[] ) => { - const aliases = await fetchAliases(callAsCurrentUser); - const hits = await fetchIndicesCall(callAsCurrentUser, indexNames); - const indices = formatHits(hits, aliases); - + const indices = await fetchIndicesCall(callAsCurrentUser, indexNames); return await indexDataEnricher.enrichIndices(indices, callAsCurrentUser); }; diff --git a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts index 8b4ae27cb3061..841528d3910b2 100644 --- a/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts +++ b/x-pack/plugins/infra/common/formatters/snapshot_metric_formats.ts @@ -70,4 +70,8 @@ export const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.number, template: '{{value}} seconds', }, + ['rdsLatency']: { + formatter: InfraFormatterType.number, + template: '{{value}} ms', + }, }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 8fdba86f233d4..a176ba756652a 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -103,9 +103,13 @@ export const Expressions: React.FC = props => { const addExpression = useCallback(() => { const exp = alertParams.criteria?.slice() || []; - exp.push(defaultExpression); + exp.push({ + ...defaultExpression, + timeSize: timeSize ?? defaultExpression.timeSize, + timeUnit: timeUnit ?? defaultExpression.timeUnit, + }); setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria]); + }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( (id: number) => { diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx index c2ee552e31553..97c0bb98962d4 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/expression.tsx @@ -110,10 +110,14 @@ export const Expressions: React.FC = props => { ); const addExpression = useCallback(() => { - const exp = alertParams.criteria.slice(); - exp.push(defaultExpression); + const exp = alertParams.criteria?.slice() || []; + exp.push({ + ...defaultExpression, + timeSize: timeSize ?? defaultExpression.timeSize, + timeUnit: timeUnit ?? defaultExpression.timeUnit, + }); setAlertParams('criteria', exp); - }, [setAlertParams, alertParams.criteria]); + }, [setAlertParams, alertParams.criteria, timeSize, timeUnit]); const removeExpression = useCallback( (id: number) => { @@ -185,6 +189,31 @@ export const Expressions: React.FC = props => { [onFilterChange] ); + const preFillAlertCriteria = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.options) { + setAlertParams('criteria', [ + { + ...defaultExpression, + metric: md.options.metric!.type, + } as InventoryMetricConditions, + ]); + } else { + setAlertParams('criteria', [defaultExpression]); + } + }, [alertsContext.metadata, setAlertParams]); + + const preFillAlertFilter = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.filter) { + setAlertParams('filterQueryText', md.filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' + ); + } + }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); + useEffect(() => { const md = alertsContext.metadata; if (!alertParams.nodeType) { @@ -195,31 +224,19 @@ export const Expressions: React.FC = props => { } } - if (!alertParams.criteria) { - if (md && md.options) { - setAlertParams('criteria', [ - { - ...defaultExpression, - metric: md.options.metric!.type, - } as InventoryMetricConditions, - ]); - } else { - setAlertParams('criteria', [defaultExpression]); - } + if (alertParams.criteria && alertParams.criteria.length) { + setTimeSize(alertParams.criteria[0].timeSize); + setTimeUnit(alertParams.criteria[0].timeUnit); + } else { + preFillAlertCriteria(); } if (!alertParams.filterQuery) { - if (md && md.filter) { - setAlertParams('filterQueryText', md.filter); - setAlertParams( - 'filterQuery', - convertKueryToElasticSearchQuery(md.filter, derivedIndexPattern) || '' - ); - } + preFillAlertFilter(); } if (!alertParams.sourceId) { - setAlertParams('sourceId', source?.id); + setAlertParams('sourceId', source?.id || 'default'); } }, [alertsContext.metadata, derivedIndexPattern, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps @@ -235,11 +252,13 @@ export const Expressions: React.FC = props => { - + + + {alertParams.criteria && @@ -425,11 +444,13 @@ export const ExpressionRow: React.FC = props => { /> {metric && ( - -
-
{metricUnit[metric]?.label || ''}
-
-
+
+ {metricUnit[metric]?.label || ''} +
)}
@@ -502,4 +523,5 @@ const metricUnit: Record = { s3UploadBytes: { label: 'bytes' }, s3DownloadBytes: { label: 'bytes' }, sqsOldestMessage: { label: 'seconds' }, + rdsLatency: { label: 'ms' }, }; diff --git a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx index faafdf1b81eed..2c72c658ce093 100644 --- a/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx +++ b/x-pack/plugins/infra/public/components/alerting/inventory/metric.tsx @@ -24,7 +24,7 @@ interface Props { metric?: { value: SnapshotMetricType; text: string }; metrics: Array<{ value: string; text: string }>; errors: IErrorObject; - onChange: (metric: SnapshotMetricType) => void; + onChange: (metric?: SnapshotMetricType) => void; popupPosition?: | 'upCenter' | 'upLeft' @@ -65,11 +65,11 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit } )} value={metric?.text || firstFieldOption.text} - isActive={aggFieldPopoverOpen || !metric} + isActive={Boolean(aggFieldPopoverOpen || (errors.metric && errors.metric.length > 0))} onClick={() => { setAggFieldPopoverOpen(true); }} - color={metric ? 'secondary' : 'danger'} + color={errors.metric?.length ? 'danger' : 'secondary'} /> } isOpen={aggFieldPopoverOpen} @@ -89,16 +89,12 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit - 0 && metric !== undefined} - error={errors.metric} - > + 0} error={errors.metric}> 0 && metric !== undefined} + isInvalid={errors.metric.length > 0} placeholder={firstFieldOption.text} options={availablefieldsOptions} noSuggestions={!availablefieldsOptions.length} @@ -110,6 +106,8 @@ export const MetricExpression = ({ metric, metrics, errors, onChange, popupPosit if (selectedOptions.length > 0) { onChange(selectedOptions[0].value as SnapshotMetricType); setAggFieldPopoverOpen(false); + } else { + onChange(); } }} /> diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx index 06dbf5315b83a..e089ae912e112 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/analysis_setup_indices_form.tsx @@ -10,15 +10,21 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { LoadingOverlayWrapper } from '../../../loading_overlay_wrapper'; import { IndexSetupRow } from './index_setup_row'; -import { AvailableIndex } from './validation'; +import { AvailableIndex, ValidationIndicesError } from './validation'; export const AnalysisSetupIndicesForm: React.FunctionComponent<{ disabled?: boolean; indices: AvailableIndex[]; isValidating: boolean; onChangeSelectedIndices: (selectedIndices: AvailableIndex[]) => void; - valid: boolean; -}> = ({ disabled = false, indices, isValidating, onChangeSelectedIndices, valid }) => { + validationErrors?: ValidationIndicesError[]; +}> = ({ + disabled = false, + indices, + isValidating, + onChangeSelectedIndices, + validationErrors = [], +}) => { const changeIsIndexSelected = useCallback( (indexName: string, isSelected: boolean) => { onChangeSelectedIndices( @@ -41,6 +47,8 @@ export const AnalysisSetupIndicesForm: React.FunctionComponent<{ [indices, onChangeSelectedIndices] ); + const isInvalid = validationErrors.length > 0; + return ( - + <> {indices.map(index => ( void; startTime: number | undefined; endTime: number | undefined; -}> = ({ disabled = false, setStartTime, setEndTime, startTime, endTime }) => { - const now = useMemo(() => moment(), []); + validationErrors?: TimeRangeValidationError[]; +}> = ({ + disabled = false, + setStartTime, + setEndTime, + startTime, + endTime, + validationErrors = [], +}) => { + const [now] = useState(() => moment()); const selectedEndTimeIsToday = !endTime || moment(endTime).isSame(now, 'day'); + const startTimeValue = useMemo(() => { return startTime ? moment(startTime) : undefined; }, [startTime]); const endTimeValue = useMemo(() => { return endTime ? moment(endTime) : undefined; }, [endTime]); + + const startTimeValidationErrorMessages = useMemo( + () => getStartTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + + const endTimeValidationErrorMessages = useMemo( + () => getEndTimeValidationErrorMessages(validationErrors), + [validationErrors] + ); + return ( } > - + 0} + label={startTimeLabel} + > setStartTime(undefined) } : undefined} @@ -91,7 +117,12 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ - + 0} + label={endTimeLabel} + > setEndTime(undefined) } : undefined} @@ -122,3 +153,31 @@ export const AnalysisSetupTimerangeForm: React.FunctionComponent<{ ); }; + +const getStartTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.startTimeBeforeEndTimeErrorMessage', { + defaultMessage: 'The start time must be before the end time.', + }), + ]; + default: + return []; + } + }); + +const getEndTimeValidationErrorMessages = (validationErrors: TimeRangeValidationError[]) => + validationErrors.flatMap(validationError => { + switch (validationError.error) { + case 'INVALID_TIME_RANGE': + return [ + i18n.translate('xpack.infra.analysisSetup.endTimeAfterStartTimeErrorMessage', { + defaultMessage: 'The end time must be after the start time.', + }), + ]; + default: + return []; + } + }); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx index 18dc2e5aa9bd1..2eb67e0c0ce76 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/index_setup_row.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback } from 'react'; import { DatasetFilter } from '../../../../../common/log_analysis'; import { IndexSetupDatasetFilter } from './index_setup_dataset_filter'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { AvailableIndex, ValidationUIError } from './validation'; export const IndexSetupRow: React.FC<{ index: AvailableIndex; @@ -61,7 +61,7 @@ export const IndexSetupRow: React.FC<{ ); }; -const formatValidationError = (errors: ValidationIndicesUIError[]): React.ReactNode => { +const formatValidationError = (errors: ValidationUIError[]): React.ReactNode => { return errors.map(error => { switch (error.error) { case 'INDEX_NOT_FOUND': diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx index 85aa7ce513248..c9b14a1ffe47a 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/initial_configuration_step.tsx @@ -4,16 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiForm, EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiForm, EuiSpacer } from '@elastic/eui'; import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useMemo } from 'react'; - import { SetupStatus } from '../../../../../common/log_analysis'; import { AnalysisSetupIndicesForm } from './analysis_setup_indices_form'; import { AnalysisSetupTimerangeForm } from './analysis_setup_timerange_form'; -import { AvailableIndex, ValidationIndicesUIError } from './validation'; +import { + AvailableIndex, + TimeRangeValidationError, + timeRangeValidationErrorRT, + ValidationIndicesError, + validationIndicesErrorRT, + ValidationUIError, +} from './validation'; interface InitialConfigurationStepProps { setStartTime: (startTime: number | undefined) => void; @@ -24,7 +30,7 @@ interface InitialConfigurationStepProps { validatedIndices: AvailableIndex[]; setupStatus: SetupStatus; setValidatedIndices: (selectedIndices: AvailableIndex[]) => void; - validationErrors?: ValidationIndicesUIError[]; + validationErrors?: ValidationUIError[]; } export const createInitialConfigurationStep = ( @@ -47,6 +53,11 @@ export const InitialConfigurationStep: React.FunctionComponent { const disabled = useMemo(() => !editableFormStatus.includes(setupStatus.type), [setupStatus]); + const [indexValidationErrors, timeRangeValidationErrors, globalValidationErrors] = useMemo( + () => partitionValidationErrors(validationErrors), + [validationErrors] + ); + return ( <> @@ -57,16 +68,17 @@ export const InitialConfigurationStep: React.FunctionComponent - + ); @@ -88,7 +100,7 @@ const initialConfigurationStepTitle = i18n.translate( } ); -const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ errors }) => { +const ValidationErrors: React.FC<{ errors: ValidationUIError[] }> = ({ errors }) => { if (errors.length === 0) { return null; } @@ -107,7 +119,7 @@ const ValidationErrors: React.FC<{ errors: ValidationIndicesUIError[] }> = ({ er ); }; -const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode => { +const formatValidationError = (error: ValidationUIError): React.ReactNode => { switch (error.error) { case 'NETWORK_ERROR': return ( @@ -129,3 +141,19 @@ const formatValidationError = (error: ValidationIndicesUIError): React.ReactNode return ''; } }; + +const partitionValidationErrors = (validationErrors: ValidationUIError[]) => + validationErrors.reduce< + [ValidationIndicesError[], TimeRangeValidationError[], ValidationUIError[]] + >( + ([indicesErrors, timeRangeErrors, otherErrors], error) => { + if (validationIndicesErrorRT.is(error)) { + return [[...indicesErrors, error], timeRangeErrors, otherErrors]; + } else if (timeRangeValidationErrorRT.is(error)) { + return [indicesErrors, [...timeRangeErrors, error], otherErrors]; + } else { + return [indicesErrors, timeRangeErrors, [...otherErrors, error]]; + } + }, + [[], [], []] + ); diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx index d69e544aeab18..4a3899f2d3918 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/initial_configuration_step/validation.tsx @@ -4,15 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ValidationIndicesError } from '../../../../../common/http_api'; +import * as rt from 'io-ts'; +import { ValidationIndicesError, validationIndicesErrorRT } from '../../../../../common/http_api'; import { DatasetFilter } from '../../../../../common/log_analysis'; -export { ValidationIndicesError }; +export { ValidationIndicesError, validationIndicesErrorRT }; -export type ValidationIndicesUIError = +export const timeRangeValidationErrorRT = rt.strict({ + error: rt.literal('INVALID_TIME_RANGE'), +}); + +export type TimeRangeValidationError = rt.TypeOf; + +export type ValidationUIError = | ValidationIndicesError | { error: 'NETWORK_ERROR' } - | { error: 'TOO_FEW_SELECTED_INDICES' }; + | { error: 'TOO_FEW_SELECTED_INDICES' } + | TimeRangeValidationError; interface ValidAvailableIndex { validity: 'valid'; diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts index d46e8bc2485f6..9f757497aff81 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_setup_state.ts @@ -16,7 +16,7 @@ import { import { AvailableIndex, ValidationIndicesError, - ValidationIndicesUIError, + ValidationUIError, } from '../../../components/logging/log_analysis_setup/initial_configuration_step'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; @@ -46,6 +46,11 @@ export const useAnalysisSetupState = ({ const [startTime, setStartTime] = useState(Date.now() - fourWeeksInMs); const [endTime, setEndTime] = useState(undefined); + const isTimeRangeValid = useMemo( + () => (startTime != null && endTime != null ? startTime < endTime : true), + [endTime, startTime] + ); + const [validatedIndices, setValidatedIndices] = useState( sourceConfiguration.indices.map(indexName => ({ name: indexName, @@ -201,35 +206,54 @@ export const useAnalysisSetupState = ({ [validateDatasetsRequest.state, validateIndicesRequest.state] ); - const validationErrors = useMemo(() => { + const validationErrors = useMemo(() => { if (isValidating) { return []; } - if (validateIndicesRequest.state === 'rejected') { - return [{ error: 'NETWORK_ERROR' }]; - } - - if (selectedIndexNames.length === 0) { - return [{ error: 'TOO_FEW_SELECTED_INDICES' }]; - } - - return validatedIndices.reduce((errors, index) => { - return index.validity === 'invalid' && selectedIndexNames.includes(index.name) - ? [...errors, ...index.errors] - : errors; - }, []); - }, [isValidating, validateIndicesRequest.state, selectedIndexNames, validatedIndices]); + return [ + // validate request status + ...(validateIndicesRequest.state === 'rejected' || + validateDatasetsRequest.state === 'rejected' + ? [{ error: 'NETWORK_ERROR' as const }] + : []), + // validation request results + ...validatedIndices.reduce((errors, index) => { + return index.validity === 'invalid' && selectedIndexNames.includes(index.name) + ? [...errors, ...index.errors] + : errors; + }, []), + // index count + ...(selectedIndexNames.length === 0 ? [{ error: 'TOO_FEW_SELECTED_INDICES' as const }] : []), + // time range + ...(!isTimeRangeValid ? [{ error: 'INVALID_TIME_RANGE' as const }] : []), + ]; + }, [ + isValidating, + validateIndicesRequest.state, + validateDatasetsRequest.state, + validatedIndices, + selectedIndexNames, + isTimeRangeValid, + ]); const prevStartTime = usePrevious(startTime); const prevEndTime = usePrevious(endTime); const prevValidIndexNames = usePrevious(validIndexNames); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + validateIndices(); - }, [validateIndices]); + }, [isTimeRangeValid, validateIndices]); useEffect(() => { + if (!isTimeRangeValid) { + return; + } + if ( startTime !== prevStartTime || endTime !== prevEndTime || @@ -239,6 +263,7 @@ export const useAnalysisSetupState = ({ } }, [ endTime, + isTimeRangeValid, prevEndTime, prevStartTime, prevValidIndexNames, diff --git a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts index aad54bd2222b7..94e2537a67a2a 100644 --- a/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts +++ b/x-pack/plugins/infra/public/containers/source/use_source_via_http.ts @@ -8,8 +8,8 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import createContainer from 'constate'; -import { HttpHandler } from 'target/types/core/public/http'; -import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; +import { HttpHandler } from 'src/core/public'; +import { ToastInput } from 'src/core/public'; import { SourceResponseRuntimeType, SourceResponse, diff --git a/x-pack/plugins/infra/public/hooks/use_http_request.tsx b/x-pack/plugins/infra/public/hooks/use_http_request.tsx index e00abe6380498..0a0c876bb63ce 100644 --- a/x-pack/plugins/infra/public/hooks/use_http_request.tsx +++ b/x-pack/plugins/infra/public/hooks/use_http_request.tsx @@ -7,8 +7,8 @@ import React, { useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { i18n } from '@kbn/i18n'; -import { HttpHandler } from 'target/types/core/public/http'; -import { ToastInput } from 'target/types/core/public/notifications/toasts/toasts_api'; +import { HttpHandler } from 'src/core/public'; +import { ToastInput } from 'src/core/public'; import { useTrackedPromise } from '../utils/use_tracked_promise'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts index f8c7a10f12831..479c292035ae5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -71,6 +71,10 @@ const METRIC_FORMATTERS: MetricFormatters = { formatter: InfraFormatterType.number, template: '{{value}} seconds', }, + ['rdsLatency']: { + formatter: InfraFormatterType.number, + template: '{{value}} ms', + }, }; export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index da6d77ef4b478..6b4ac8b1ba060 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -7,7 +7,7 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; -import { HttpHandler } from 'target/types/core/public/http'; +import { HttpHandler } from 'src/core/public'; import { IIndexPattern } from 'src/plugins/data/public'; import { SourceQuery } from '../../../../../common/graphql/types'; import { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index e9b736e379b58..2936eea21805d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -14,17 +14,6 @@ export { DATASOURCE_SAVED_OBJECT_TYPE, } from '../../../../common'; -export const BASE_PATH = '/app/ingestManager'; -export const EPM_PATH = '/epm'; -export const EPM_LIST_ALL_PACKAGES_PATH = EPM_PATH; -export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; -export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; -export const AGENT_CONFIG_PATH = '/configs'; -export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; -export const DATA_STREAM_PATH = '/data-streams'; -export const FLEET_PATH = '/fleet'; -export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; -export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; -export const FLEET_ENROLLMENT_TOKENS_PATH = `/fleet/enrollment-tokens`; +export * from './page_paths'; export const INDEX_NAME = '.kibana'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts new file mode 100644 index 0000000000000..73771fa3cb343 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type StaticPage = + | 'overview' + | 'integrations' + | 'integrations_all' + | 'integrations_installed' + | 'configurations' + | 'configurations_list' + | 'fleet' + | 'fleet_enrollment_tokens' + | 'data_streams'; + +export type DynamicPage = + | 'integration_details' + | 'configuration_details' + | 'add_datasource_from_configuration' + | 'add_datasource_from_integration' + | 'edit_datasource' + | 'fleet_agent_list' + | 'fleet_agent_details'; + +export type Page = StaticPage | DynamicPage; + +export interface DynamicPagePathValues { + [key: string]: string; +} + +export const BASE_PATH = '/app/ingestManager'; + +// If routing paths are changed here, please also check to see if +// `pagePathGetters()`, below, needs any modifications +export const PAGE_ROUTING_PATHS = { + overview: '/', + integrations: '/integrations/:tabId?', + integrations_all: '/integrations', + integrations_installed: '/integrations/installed', + integration_details: '/integrations/detail/:pkgkey/:panel?', + configurations: '/configs', + configurations_list: '/configs', + configuration_details: '/configs/:configId/:tabId?', + configuration_details_yaml: '/configs/:configId/yaml', + configuration_details_settings: '/configs/:configId/settings', + add_datasource_from_configuration: '/configs/:configId/add-datasource', + add_datasource_from_integration: '/integrations/:pkgkey/add-datasource', + edit_datasource: '/configs/:configId/edit-datasource/:datasourceId', + fleet: '/fleet', + fleet_agent_list: '/fleet/agents', + fleet_agent_details: '/fleet/agents/:agentId/:tabId?', + fleet_agent_details_events: '/fleet/agents/:agentId', + fleet_agent_details_details: '/fleet/agents/:agentId/details', + fleet_enrollment_tokens: '/fleet/enrollment-tokens', + data_streams: '/data-streams', +}; + +export const pagePathGetters: { + [key in StaticPage]: () => string; +} & + { + [key in DynamicPage]: (values: DynamicPagePathValues) => string; + } = { + overview: () => '/', + integrations: () => '/integrations', + integrations_all: () => '/integrations', + integrations_installed: () => '/integrations/installed', + integration_details: ({ pkgkey, panel }) => + `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + configurations: () => '/configs', + configurations_list: () => '/configs', + configuration_details: ({ configId, tabId }) => `/configs/${configId}${tabId ? `/${tabId}` : ''}`, + add_datasource_from_configuration: ({ configId }) => `/configs/${configId}/add-datasource`, + add_datasource_from_integration: ({ pkgkey }) => `/integrations/${pkgkey}/add-datasource`, + edit_datasource: ({ configId, datasourceId }) => + `/configs/${configId}/edit-datasource/${datasourceId}`, + fleet: () => '/fleet', + fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, + fleet_agent_details: ({ agentId, tabId }) => + `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, + fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', + data_streams: () => '/data-streams', +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 66c7333150fb7..a752ad2a8912b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities'; export { useCore, CoreContext } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; +export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; @@ -15,3 +16,4 @@ export { useDebounce } from './use_debounce'; export * from './use_request'; export * from './use_input'; export * from './use_url_params'; +export * from './use_fleet_status'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index ff6656e969c93..207c757fd5b16 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -3,11 +3,225 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb } from 'src/core/public'; +import { BASE_PATH, Page, DynamicPagePathValues, pagePathGetters } from '../constants'; import { useCore } from './use_core'; -export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) { - const { chrome } = useCore(); - return chrome.setBreadcrumbs(newBreadcrumbs); +const BASE_BREADCRUMB: ChromeBreadcrumb = { + href: pagePathGetters.overview(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', { + defaultMessage: 'Ingest Manager', + }), +}; + +const breadcrumbGetters: { + [key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; +} = { + overview: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.overviewPageTitle', { + defaultMessage: 'Overview', + }), + }, + ], + integrations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + ], + integrations_all: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle', { + defaultMessage: 'All', + }), + }, + ], + integrations_installed: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle', { + defaultMessage: 'Installed', + }), + }, + ], + integration_details: ({ pkgTitle }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { text: pkgTitle }, + ], + configurations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configurations_list: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configuration_details: ({ configName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { text: configName }, + ], + add_datasource_from_configuration: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + add_datasource_from_integration: ({ pkgTitle, pkgkey }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + href: pagePathGetters.integration_details({ pkgkey }), + text: pkgTitle, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + edit_datasource: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.editDatasourcePageTitle', { + defaultMessage: 'Edit data source', + }), + }, + ], + fleet: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + ], + fleet_agent_list: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + ], + fleet_agent_details: ({ agentHost }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + { text: agentHost }, + ], + fleet_enrollment_tokens: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', { + defaultMessage: 'Enrollment tokens', + }), + }, + ], + data_streams: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { + defaultMessage: 'Data streams', + }), + }, + ], +}; + +export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { + const { chrome, http } = useCore(); + const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map(breadcrumb => ({ + ...breadcrumb, + href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, + })); + const docTitle: string[] = [...breadcrumbs] + .reverse() + .map(breadcrumb => breadcrumb.text as string); + chrome.docTitle.change(docTitle); + chrome.setBreadcrumbs(breadcrumbs); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts index f6c5b8bc03fce..58537b2075c16 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts @@ -6,9 +6,9 @@ import { useCore } from './'; -const BASE_PATH = '/app/kibana'; +const KIBANA_BASE_PATH = '/app/kibana'; export function useKibanaLink(path: string = '/') { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts index 333606cec8028..1b17c5cb0b1f3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts @@ -4,10 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../constants'; +import { + BASE_PATH, + StaticPage, + DynamicPage, + DynamicPagePathValues, + pagePathGetters, +} from '../constants'; import { useCore } from './'; -export function useLink(path: string = '/') { +const getPath = (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { + return values ? pagePathGetters[page](values) : pagePathGetters[page as StaticPage](); +}; + +export const useLink = () => { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); -} + return { + getPath, + getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => { + const path = getPath(page, values); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + }, + }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 3612497e723cd..f6a386314272f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -18,7 +18,7 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; +import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; @@ -174,42 +174,42 @@ const IngestManagerRoutes = ({ ...rest }) => { } return ( - + - - + + - + - + - + - + - + - - + + - + ); }; @@ -265,3 +265,8 @@ export function renderApp( ReactDOM.unmountComponentAtNode(element); }; } + +export const teardownIngestManager = (coreStart: CoreStart) => { + coreStart.chrome.docTitle.reset(); + coreStart.chrome.setBreadcrumbs([]); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index e9d7fcb1cf5c5..fbe7c736e2df4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { showSettings?: boolean; @@ -39,8 +38,8 @@ export const DefaultLayout: React.FunctionComponent = ({ section, children, }) => { + const { getHref } = useLink(); const { epm, fleet } = useConfig(); - const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); return ( @@ -60,7 +59,7 @@ export const DefaultLayout: React.FunctionComponent = ({ - + = ({ = ({ defaultMessage="Integrations" /> - + = ({ = ({ defaultMessage="Fleet" /> - + ( ({ count, agentConfigId }) => { - const FLEET_URI = useLink(FLEET_AGENTS_PATH); + const { getHref } = useLink(); const displayValue = ( ( /> ); return count > 0 ? ( - + {displayValue} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 46233fdb59509..577f08cdc3313 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,16 +17,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, sendCreateDatasource, useCore, useConfig, sendGetAgentStatus, } from '../../../hooks'; -import { useLinks as useEPMLinks } from '../../epm/hooks'; import { ConfirmDeployConfigModal } from '../components'; import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, DatasourceFormState } from './types'; @@ -48,6 +47,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { params: { configId, pkgkey }, } = useRouteMatch(); + const { getHref, getPath } = useLink(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -95,32 +95,46 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { // Datasource validation state const [validationResults, setValidationResults] = useState(); + // Form state + const [formState, setFormState] = useState('INVALID'); + // Update package info method - const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { - if (updatedPackageInfo) { - setPackageInfo(updatedPackageInfo); - setFormState('VALID'); - } else { - setFormState('INVALID'); - setPackageInfo(undefined); - } + const updatePackageInfo = useCallback( + (updatedPackageInfo: PackageInfo | undefined) => { + if (updatedPackageInfo) { + setPackageInfo(updatedPackageInfo); + if (agentConfig) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setPackageInfo(undefined); + } - // eslint-disable-next-line no-console - console.debug('Package info updated', updatedPackageInfo); - }; + // eslint-disable-next-line no-console + console.debug('Package info updated', updatedPackageInfo); + }, + [agentConfig, setPackageInfo, setFormState] + ); // Update agent config method - const updateAgentConfig = (updatedAgentConfig: AgentConfig | undefined) => { - if (updatedAgentConfig) { - setAgentConfig(updatedAgentConfig); - } else { - setFormState('INVALID'); - setAgentConfig(undefined); - } + const updateAgentConfig = useCallback( + (updatedAgentConfig: AgentConfig | undefined) => { + if (updatedAgentConfig) { + setAgentConfig(updatedAgentConfig); + if (packageInfo) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setAgentConfig(undefined); + } - // eslint-disable-next-line no-console - console.debug('Agent config updated', updatedAgentConfig); - }; + // eslint-disable-next-line no-console + console.debug('Agent config updated', updatedAgentConfig); + }, + [packageInfo, setAgentConfig, setFormState] + ); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; @@ -156,18 +170,13 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } }; - // Cancel url - const CONFIG_URL = useLink( - `${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}` - ); - const PACKAGE_URL = useEPMLinks().toDetailView({ - name: (pkgkey || '-').split('-')[0], - version: (pkgkey || '-').split('-')[1], - }); - const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; + // Cancel path + const cancelUrl = + from === 'config' + ? getHref('configuration_details', { configId: agentConfig?.id || configId }) + : getHref('integration_details', { pkgkey }); // Save datasource - const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -186,7 +195,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + history.push(getPath('configuration_details', { configId: agentConfig?.id || configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { defaultMessage: `Successfully added '{datasourceName}'`, @@ -219,33 +228,43 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo, }; + const stepSelectConfig = useMemo( + () => ( + + ), + [pkgkey, updatePackageInfo, agentConfig, updateAgentConfig] + ); + + const stepSelectPackage = useMemo( + () => ( + + ), + [configId, updateAgentConfig, packageInfo, updatePackageInfo] + ); + const steps: EuiStepProps[] = [ from === 'package' ? { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle', { defaultMessage: 'Select an agent configuration', }), - children: ( - - ), + children: stepSelectConfig, } : { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageTitle', { defaultMessage: 'Select an integration', }), - children: ( - - ), + children: stepSelectPackage, }, { title: i18n.translate('xpack.ingestManager.createDatasource.stepDefineDatasourceTitle', { @@ -280,6 +299,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ) : null, }, ]; + return ( {formState === 'CONFIRM' && agentConfig && ( @@ -290,6 +310,16 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} + {from === 'package' + ? packageInfo && ( + + ) + : agentConfig && ( + + )} {/* TODO #64541 - Remove classes */} @@ -331,3 +361,19 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ); }; + +const ConfigurationBreadcrumb: React.FunctionComponent<{ + configName: string; + configId: string; +}> = ({ configName, configId }) => { + useBreadcrumbs('add_datasource_from_configuration', { configName, configId }); + return null; +}; + +const IntegrationBreadcrumb: React.FunctionComponent<{ + pkgTitle: string; + pkgkey: string; +}> = ({ pkgTitle, pkgkey }) => { + useBreadcrumbs('add_datasource_from_integration', { pkgTitle, pkgkey }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index a0418c5f256c4..3ad862c5e43fd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -20,7 +20,6 @@ import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; import { useCapabilities, useLink } from '../../../../../hooks'; -import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; import { PackageIcon } from '../../../../../components/package_icon'; @@ -54,9 +53,8 @@ export const DatasourcesTable: React.FunctionComponent = ({ config, ...rest }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); - const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -216,7 +214,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ = ({ ], }, ], - [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] + [config, getHref, hasWriteCapabilities, refreshConfig] ); return ( @@ -274,9 +275,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ search={{ toolsRight: [ (({ configId }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId }); return ( (({ configId }) => { /> } actions={ - + ( fleet: { enabled: isFleetEnabled }, } = useConfig(); const history = useHistory(); + const { getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const refreshConfig = useConfigRefresh(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -147,7 +148,7 @@ export const ConfigSettingsView = memo<{ config: AgentConfig }>( validation={validation} isEditing={true} onDelete={() => { - history.push(AGENT_CONFIG_PATH); + history.push(getPath('configurations_list')); }} /> {/* TODO #64541 - Remove classes */} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts deleted file mode 100644 index 787791f985c7d..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; - -export const DETAILS_ROUTER_PATH = `${AGENT_CONFIG_DETAILS_PATH}:configId`; -export const DETAILS_ROUTER_SUB_PATH = `${DETAILS_ROUTER_PATH}/:tabId`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts deleted file mode 100644 index 9332ce3e0f909..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { generatePath } from 'react-router-dom'; -import { useLink } from '../../../../hooks'; -import { AGENT_CONFIG_PATH } from '../../../../constants'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from '../constants'; - -type AgentConfigUriArgs = - | ['list'] - | ['details', { configId: string }] - | ['details-yaml', { configId: string }] - | ['details-settings', { configId: string }] - | ['datasource', { configId: string; datasourceId: string }] - | ['add-datasource', { configId: string }]; - -/** - * Returns a Uri that starts at the Agent Config Route path (`/configs/`). - * These are good for use when needing to use React Router's redirect or - * `history.push(routePath)`. - * @param args - */ -export const useAgentConfigUri = (...args: AgentConfigUriArgs) => { - switch (args[0]) { - case 'list': - return AGENT_CONFIG_PATH; - case 'details': - return generatePath(DETAILS_ROUTER_PATH, args[1]); - case 'details-yaml': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'yaml' })}`; - case 'details-settings': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'settings' })}`; - case 'add-datasource': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'add-datasource' })}`; - case 'datasource': - const [, options] = args; - return `${generatePath(DETAILS_ROUTER_PATH, options)}?datasourceId=${options.datasourceId}`; - } - return '/'; -}; - -/** - * Returns a full Link that includes Kibana basepath (ex. `/app/ingestManager#/configs`). - * These are good for use in `href` properties - * @param args - */ -export const useAgentConfigLink = (...args: AgentConfigUriArgs) => { - const BASE_URI = useLink(''); - const AGENT_CONFIG_ROUTE = useAgentConfigUri(...args); - return `${BASE_URI}${AGENT_CONFIG_ROUTE}`; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 82879c174b7d3..f80b981b69d3b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -21,13 +21,13 @@ import { } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import styled from 'styled-components'; -import { useGetOneAgentConfig } from '../../../hooks'; +import { AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; +import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from './hooks/use_details_uri'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; import { ConfigDatasourcesView } from './components/datasources'; import { ConfigYamlView } from './components/yaml'; import { ConfigSettingsView } from './components/settings'; @@ -38,23 +38,11 @@ const Divider = styled.div` border-left: ${props => props.theme.eui.euiBorderThin}; `; -export const AgentConfigDetailsPage = memo(() => { - return ( - - - - - - - - - ); -}); - -export const AgentConfigDetailsLayout: React.FunctionComponent = () => { +export const AgentConfigDetailsPage: React.FunctionComponent = () => { const { params: { configId, tabId = '' }, } = useRouteMatch<{ configId: string; tabId?: string }>(); + const { getHref } = useLink(); const agentConfigRequest = useGetOneAgentConfig(configId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; const { isLoading, error, sendRequest: refreshAgentConfig } = agentConfigRequest; @@ -63,17 +51,16 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const { refreshAgentStatus } = agentStatusRequest; const agentStatus = agentStatusRequest.data?.results; - // Links - const configListLink = useAgentConfigLink('list'); - const configDetailsLink = useAgentConfigLink('details', { configId }); - const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); - const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); - const headerLeftContent = useMemo( () => ( - + { ) : null} ), - [configListLink, agentConfig, configId] + [getHref, agentConfig, configId] ); const headerRightContent = useMemo( @@ -184,7 +171,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { defaultMessage: 'Data sources', }), - href: configDetailsLink, + href: getHref('configuration_details', { configId, tabId: 'datasources' }), isSelected: tabId === '' || tabId === 'datasources', }, { @@ -192,7 +179,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { defaultMessage: 'YAML', }), - href: configDetailsYamlLink, + href: getHref('configuration_details', { configId, tabId: 'yaml' }), isSelected: tabId === 'yaml', }, { @@ -200,11 +187,11 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), - href: configDetailsSettingsLink, + href: getHref('configuration_details', { configId, tabId: 'settings' }), isSelected: tabId === 'settings', }, ]; - }, [configDetailsLink, configDetailsSettingsLink, configDetailsYamlLink, tabId]); + }, [getHref, configId, tabId]); if (redirectToAgentConfigList) { return ; @@ -254,28 +241,37 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - - { - return ; - }} - /> - { - return ; - }} - /> - { - return ; - }} - /> - + ); }; + +const AgentConfigDetailsContent: React.FunctionComponent<{ agentConfig: AgentConfig }> = ({ + agentConfig, +}) => { + useBreadcrumbs('configuration_details', { configName: agentConfig.name }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index 089a5a91df88a..92be20a2761e2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -16,10 +16,10 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, useCore, useConfig, sendUpdateDatasource, @@ -53,6 +53,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { params: { configId, datasourceId }, } = useRouteMatch(); const history = useHistory(); + const { getHref, getPath } = useLink(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); useEffect(() => { @@ -185,8 +186,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; // Cancel url - const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); - const cancelUrl = CONFIG_URL; + const cancelUrl = getHref('configuration_details', { configId }); // Save datasource const [formState, setFormState] = useState('INVALID'); @@ -208,7 +208,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + history.push(getPath('configuration_details', { configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { defaultMessage: `Successfully updated '{datasourceName}'`, @@ -262,6 +262,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { /> ) : ( <> + {formState === 'CONFIRM' && ( { ); }; + +const Breadcrumb: React.FunctionComponent<{ configName: string; configId: string }> = ({ + configName, + configId, +}) => { + useBreadcrumbs('edit_datasource', { configName, configId }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index ef88aa5d17f1e..74fa67078f741 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -5,26 +5,32 @@ */ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useBreadcrumbs } from '../../hooks'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; import { EditDatasourcePage } from './edit_datasource_page'; -export const AgentConfigApp: React.FunctionComponent = () => ( - - - - - - - - - - - - - - - - -); +export const AgentConfigApp: React.FunctionComponent = () => { + useBreadcrumbs('configurations'); + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 9b565a0452c96..ff3124d574857 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; import { AgentConfig } from '../../../types'; -import { - AGENT_CONFIG_DETAILS_PATH, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - AGENT_CONFIG_PATH, -} from '../../../constants'; +import { AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, @@ -35,11 +31,11 @@ import { useLink, useConfig, useUrlParams, + useBreadcrumbs, } from '../../../hooks'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ @@ -81,14 +77,17 @@ const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( ({ config, onDelete }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const detailsLink = useAgentConfigLink('details', { configId: config.id }); - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); return ( + void }>( void }>( ); export const AgentConfigListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('configurations_list'); + const { getHref, getPath } = useLink(); // Config information const hasWriteCapabilites = useCapabilities().write; const { fleet: { enabled: isFleetEnabled }, } = useConfig(); - // Base URL paths - const DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - // Table and search states const { urlParams, toUrlParams } = useUrlParams(); const [search, setSearch] = useState( @@ -142,14 +140,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { (isOpen: boolean) => { if (isOpen !== isCreateAgentConfigFlyoutOpen) { if (isOpen) { - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams({ ...urlParams, create: null })}`); + history.push( + `${getPath('configurations_list')}?${toUrlParams({ ...urlParams, create: null })}` + ); } else { const { create, ...params } = urlParams; - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams(params)}`); + history.push(`${getPath('configurations_list')}?${toUrlParams(params)}`); } } }, - [history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] + [getPath, history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] ); // Fetch agent configs @@ -174,7 +174,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { @@ -253,7 +253,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { } return cols; - }, [DETAILS_URI, isFleetEnabled, sendRequest]); + }, [getHref, isFleetEnabled, sendRequest]); const createAgentConfigButton = useMemo( () => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx index 7b0641e66fd43..0fdba54a04145 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { DataStreamListPage } from './list_page'; export const DataStreamApp: React.FunctionComponent = () => { return ( - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index cff138c6a16ca..09873a3cdaa87 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; -import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; +import { useGetDataStreams, useStartDeps, usePagination, useBreadcrumbs } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; import { DataStreamRowActions } from './components/data_stream_row_actions'; @@ -55,6 +55,8 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( ); export const DataStreamListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('data_streams'); + const { data: { fieldFormats }, } = useStartDeps(); @@ -239,7 +241,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { sorting={true} search={{ toolsRight: [ - sendRequest()}> + sendRequest()} + > } - href={url} + href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx index d4ed3624a6e68..436163bafcfe4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx @@ -3,32 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { generatePath } from 'react-router-dom'; import { useCore } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; -import { DetailViewPanelName } from '../../../types'; -import { BASE_PATH, EPM_PATH, EPM_DETAIL_VIEW_PATH } from '../../../constants'; - -// TODO: get this from server/packages/handlers.ts (move elsewhere?) -// seems like part of the name@version change -interface DetailParams { - name: string; - version: string; - panel?: DetailViewPanelName; - withAppRoot?: boolean; -} const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; export function useLinks() { const { http } = useCore(); - function appRoot(path: string) { - // include '#' because we're using HashRouter - return http.basePath.prepend(BASE_PATH + '#' + path); - } - return { toAssets: (path: string) => http.basePath.prepend( @@ -49,13 +32,5 @@ export function useLinks() { const filePath = `${epmRouteService.getInfoPath(pkgkey)}/${imagePath}`; return http.basePath.prepend(filePath); }, - toListView: () => appRoot(EPM_PATH), - toDetailView: ({ name, version, panel, withAppRoot = true }: DetailParams) => { - // panel is optional, but `generatePath` won't accept `path: undefined` - // so use this to pass `{ pkgkey }` or `{ pkgkey, panel }` - const params = Object.assign({ pkgkey: `${name}-${version}` }, panel ? { panel } : {}); - const path = generatePath(EPM_DETAIL_VIEW_PATH, params); - return withAppRoot ? appRoot(path) : path; - }, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 244a9a2c7426e..36b81e786b935 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -6,12 +6,12 @@ import createContainer from 'constate'; import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; -import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; -import { useLinks } from '.'; +import { sendInstallPackage, sendRemovePackage, useLink } from '../../../hooks'; import { InstallStatus } from '../../../types'; interface PackagesInstall { @@ -29,7 +29,8 @@ type InstallPackageProps = Pick & { type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { - const { toDetailView } = useLinks(); + const history = useHistory(); + const { getPath } = useLink(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( @@ -88,12 +89,11 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar } else { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); if (fromUpdate) { - const settingsUrl = toDetailView({ - name, - version, + const settingsPath = getPath('integration_details', { + pkgkey: `${name}-${version}`, panel: 'settings', }); - window.location.href = settingsUrl; + history.push(settingsPath); } notifications.toasts.addSuccess({ title: toMountPoint( @@ -113,7 +113,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); } }, - [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, getPath, history] ); const uninstallPackage = useCallback( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx index 2c8ee7ca2fcf3..ca1a8df534044 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx @@ -6,24 +6,26 @@ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; -import { useConfig } from '../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useConfig, useBreadcrumbs } from '../../hooks'; import { CreateDatasourcePage } from '../agent_config/create_datasource_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; export const EPMApp: React.FunctionComponent = () => { + useBreadcrumbs('integrations'); const { epm } = useConfig(); return epm.enabled ? ( - + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index c82b7ed2297a7..7459c943fa831 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -7,29 +7,22 @@ import React, { Fragment } from 'react'; import { EuiTitle } from '@elastic/eui'; import { Redirect } from 'react-router-dom'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus } from '../../../../types'; +import { useLink } from '../../../../hooks'; interface DataSourcesPanelProps { name: string; version: string; } export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { - const { toDetailView } = useLinks(); + const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) - return ( - - ); + return ; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index cf51296d468a9..5c2d1373d0b0e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui'; import { PackageInfo } from '../../../../types'; -import { EPM_PATH } from '../../../../constants'; import { useCapabilities, useLink } from '../../../../hooks'; import { IconPanel } from '../../components/icon_panel'; import { NavButtonBack } from '../../components/nav_button_back'; -import { useLinks } from '../../hooks'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { UpdateIcon } from '../../components/icons'; @@ -36,14 +34,13 @@ export function Header(props: HeaderProps) { installedVersion = props.savedObject.attributes.version; } const hasWriteCapabilites = useCapabilities().write; - const { toListView } = useLinks(); - const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); + const { getHref } = useLink(); const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( & Pick; export function DetailLayout(props: LayoutProps) { - const { name: packageName, version, icons, restrictWidth } = props; + const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props; const iconType = usePackageIconType({ packageName, version, icons }); + useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index aa63cf2ba175d..65a437269ec6a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -8,7 +8,8 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui'; import { PackageInfo, entries, DetailViewPanelName, InstallStatus } from '../../../../types'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useLink } from '../../../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; export type NavLinkProps = Pick & { active: DetailViewPanelName; @@ -27,7 +28,7 @@ const PanelDisplayNames: Record = { }; export function SideNavLinks({ name, version, active }: NavLinkProps) { - const { toDetailView } = useLinks(); + const { getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -35,7 +36,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { {entries(PanelDisplayNames).map(([panel, display]) => { const Link = styled(EuiButtonEmpty).attrs({ - href: toDetailView({ name, version, panel }), + href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }), })` font-weight: ${p => active === panel 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 983a322de1088..84ad3593a5bf1 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,11 +8,8 @@ import React, { useState } from 'react'; import { useRouteMatch, Switch, Route } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; -import { - EPM_LIST_ALL_PACKAGES_PATH, - EPM_LIST_INSTALLED_PACKAGES_PATH, -} from '../../../../constants'; -import { useLink, useGetCategories, useGetPackages } from '../../../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../../../constants'; +import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; import { CategorySummaryItem } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; @@ -23,9 +20,7 @@ export function EPMHomePage() { const { params: { tabId }, } = useRouteMatch<{ tabId?: string }>(); - - const ALL_PACKAGES_URI = useLink(EPM_LIST_ALL_PACKAGES_PATH); - const INSTALLED_PACKAGES_URI = useLink(EPM_LIST_INSTALLED_PACKAGES_PATH); + const { getHref } = useLink(); return ( - + - + @@ -65,6 +60,7 @@ export function EPMHomePage() { } function InstalledPackages() { + useBreadcrumbs('integrations_installed'); const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); @@ -117,6 +113,7 @@ function InstalledPackages() { } function AvailablePackages() { + useBreadcrumbs('integrations_all'); const [selectedCategory, setSelectedCategory] = useState(''); const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ category: selectedCategory, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx index 6a1e6dc226903..03f1a67fe95ab 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Agent, AgentConfig } from '../../../../types'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../../constants'; import { useLink } from '../../../../hooks'; import { AgentHealth } from '../../components'; @@ -22,7 +21,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ agent: Agent; agentConfig?: AgentConfig; }> = memo(({ agent, agentConfig }) => { - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); + const { getHref } = useLink(); return ( {[ @@ -53,7 +52,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ defaultMessage: 'Agent configuration', }), description: agentConfig ? ( - + {agentConfig.name || agent.config_id} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index aa46f7cf976cd..2ebc495d5dda7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -19,16 +19,13 @@ import { import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AgentRefreshContext } from './hooks'; -import { - FLEET_AGENTS_PATH, - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, -} from '../../../constants'; +import { Agent, AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; import { Loading, Error } from '../../../components'; -import { useGetOneAgent, useGetOneAgentConfig, useLink } from '../../../hooks'; +import { useGetOneAgent, useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { WithHeaderLayout } from '../../../layouts'; import { AgentHealth } from '../components'; +import { AgentRefreshContext } from './hooks'; import { AgentEventsTable, AgentDetailsActionMenu, AgentDetailsContent } from './components'; const Divider = styled.div` @@ -41,6 +38,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { const { params: { agentId, tabId = '' }, } = useRouteMatch<{ agentId: string; tabId?: string }>(); + const { getHref } = useLink(); const { isLoading, isInitialRequest, @@ -56,16 +54,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { sendRequest: sendAgentConfigRequest, } = useGetOneAgentConfig(agentData?.item?.config_id); - const agentListUrl = useLink(FLEET_AGENTS_PATH); - const agentActivityTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/activity`); - const agentDetailsTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/details`); - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); - const headerLeftContent = useMemo( () => ( - + { ), - [agentData, agentId, agentListUrl] + [agentData, agentId, getHref] ); const headerRightContent = useMemo( @@ -114,7 +112,9 @@ export const AgentDetailsPage: React.FunctionComponent = () => { content: isAgentConfigLoading ? ( ) : agentConfigData?.item ? ( - + {agentConfigData.item.name || agentData.item.config_id} ) : ( @@ -143,7 +143,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ) : ( undefined ), - [agentConfigData, agentConfigUrl, agentData, isAgentConfigLoading] + [agentConfigData, agentData, getHref, isAgentConfigLoading] ); const headerTabs = useMemo(() => { @@ -153,7 +153,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.activityLogTab', { defaultMessage: 'Activity log', }), - href: agentActivityTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'activity' }), isSelected: !tabId || tabId === 'activity', }, { @@ -161,11 +161,11 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.detailsTab', { defaultMessage: 'Agent details', }), - href: agentDetailsTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'details' }), isSelected: tabId === 'details', }, ]; - }, [agentActivityTabUrl, agentDetailsTabUrl, tabId]); + }, [getHref, agentId, tabId]); return ( { error={error} /> ) : agentData && agentData.item ? ( - - { - return ( - - ); - }} - /> - { - return ; - }} - /> - + ) : ( { error={i18n.translate( 'xpack.ingestManager.agentDetails.agentNotFoundErrorDescription', { - defaultMessage: 'Cannot found agent ID {agentId}', + defaultMessage: 'Cannot find agent ID {agentId}', values: { agentId, }, @@ -233,3 +218,32 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ); }; + +const AgentDetailsPageContent: React.FunctionComponent<{ + agent: Agent; + agentConfig?: AgentConfig; +}> = ({ agent, agentConfig }) => { + useBreadcrumbs('fleet_agent_details', { + agentHost: + typeof agent.local_metadata.host === 'object' && + typeof agent.local_metadata.host.hostname === 'string' + ? agent.local_metadata.host.hostname + : '-', + }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 84056df2aca32..56cc0028f0cf9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -34,17 +34,14 @@ import { useGetAgents, useUrlParams, useLink, + useBreadcrumbs, } from '../../../hooks'; -import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; +import { AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; import { AgentStatusKueryHelper } from '../../../services'; -import { - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, - AGENT_SAVED_OBJECT_TYPE, -} from '../../../constants'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -77,8 +74,8 @@ const statusFilters = [ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( ({ agent, refresh, onReassignClick }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); const [isOpen, setIsOpen] = useState(false); const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); @@ -101,7 +98,11 @@ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refre > + = () => { + useBreadcrumbs('fleet_agent_list'); + const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasWriteCapabilites = useCapabilities().write; + // Agent data states const [showInactive, setShowInactive] = useState(false); @@ -241,8 +245,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const { isLoading: isAgentConfigsLoading } = agentConfigsRequest; - const CONFIG_DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - const columns = [ { field: 'local_metadata.host.hostname', @@ -250,9 +252,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Host', }), render: (host: string, agent: Agent) => ( - + {safeMetadata(host)} - + ), }, { @@ -274,7 +276,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index ff7c2f705e7b7..43173124d6bae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -23,10 +23,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../../types'; import { EnrollmentStepAgentConfig } from './config_selection'; -import { useGetOneEnrollmentAPIKey, useCore, useGetSettings, useLink } from '../../../../hooks'; +import { + useGetOneEnrollmentAPIKey, + useCore, + useGetSettings, + useLink, + useFleetStatus, +} from '../../../../hooks'; import { ManualInstructions } from '../../../../components/enrollment_instructions'; -import { FLEET_PATH } from '../../../../constants'; -import { useFleetStatus } from '../../../../hooks/use_fleet_status'; interface Props { onClose: () => void; @@ -37,9 +41,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentConfigs = [], }) => { + const { getHref } = useLink(); const core = useCore(); const fleetStatus = useFleetStatus(); - const fleetLink = useLink(FLEET_PATH); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); @@ -120,7 +124,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ defaultMessage="Fleet needs to be set up before agents can be enrolled. {link}" values={{ link: ( - + = ({ children }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; const agentStatusRequest = useGetAgentStatus(undefined, { pollIntervalMs: REFRESH_INTERVAL_MS, @@ -163,8 +164,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Agents" /> ), - isSelected: routeMatch.path === FLEET_AGENTS_PATH, - href: useLink(FLEET_AGENTS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_agent_list, + href: getHref('fleet_agent_list'), }, { name: ( @@ -173,8 +174,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Enrollment tokens" /> ), - isSelected: routeMatch.path === FLEET_ENROLLMENT_TOKENS_PATH, - href: useLink(FLEET_ENROLLMENT_TOKENS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_enrollment_tokens, + href: getHref('fleet_enrollment_tokens'), }, ] as unknown) as EuiTabProps[] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx deleted file mode 100644 index 8af0e0a5cbc25..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -interface RouteConfig { - path: string; - component: React.ComponentType; - routes?: RouteConfig[]; -} - -export const ChildRoutes: React.FunctionComponent<{ - routes?: RouteConfig[]; - useSwitch?: boolean; - [other: string]: any; -}> = ({ routes, useSwitch = true, ...rest }) => { - if (!routes) { - return null; - } - const Parent = useSwitch ? Switch : React.Fragment; - return ( - - {routes.map(route => ( - { - const Component = route.component; - return ; - }} - /> - ))} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx deleted file mode 100644 index 489ee85ffe28a..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link, withRouter } from 'react-router-dom'; - -export function ConnectedLinkComponent({ - location, - path, - query, - disabled, - children, - ...props -}: { - location: any; - path: string; - disabled: boolean; - query: any; - [key: string]: any; -}) { - if (disabled) { - return ; - } - - // Shorthand for pathname - const pathname = path || _.get(props.to, 'pathname') || location.pathname; - - return ( - - ); -} - -export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index c11e3a49c7693..add495ce0c194 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -20,6 +20,7 @@ import { import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../../constants'; import { + useBreadcrumbs, usePagination, useGetEnrollmentAPIKeys, useGetAgentConfigs, @@ -125,6 +126,7 @@ const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: }; export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('fleet_enrollment_tokens'); const [flyoutOpen, setFlyoutOpen] = useState(false); const [search, setSearch] = useState(''); const { pagination, setPagination, pageSizeOptions } = usePagination(); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index c820a9b867b63..9bb77ca44b848 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -5,17 +5,18 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { Loading } from '../../components'; -import { useConfig, useCore } from '../../hooks'; +import { useConfig, useCore, useFleetStatus, useBreadcrumbs } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; -import { useFleetStatus } from '../../hooks/use_fleet_status'; export const FleetApp: React.FunctionComponent = () => { + useBreadcrumbs('fleet'); const core = useCore(); const { fleet } = useConfig(); @@ -41,16 +42,20 @@ export const FleetApp: React.FunctionComponent = () => { return ( - } /> - + } + /> + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx index 0f6d3c5b55ce6..6e61a55466e87 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -16,10 +16,10 @@ import { import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; import { useLink, useGetAgentStatus } from '../../../hooks'; -import { FLEET_PATH } from '../../../constants'; import { Loading } from '../../fleet/components'; export const OverviewAgentSection = () => { + const { getHref } = useLink(); const agentStatusRequest = useGetAgentStatus({}); return ( @@ -34,7 +34,7 @@ export const OverviewAgentSection = () => { /> - + = ({ agentConfigs, }) => { + const { getHref } = useLink(); const datasourcesRequest = useGetDatasources({ page: 1, perPage: 10000, @@ -40,7 +40,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ /> - + { + const { getHref } = useLink(); const datastreamRequest = useGetDataStreams(); const { data: { fieldFormats }, @@ -55,7 +55,7 @@ export const OverviewDatastreamSection: React.FC = () => { /> - + { + const { getHref } = useLink(); const packagesRequest = useGetPackages(); const res = packagesRequest.data?.response; const total = res?.length ?? 0; @@ -40,7 +40,7 @@ export const OverviewIntegrationSection: React.FC = () => { /> - + { + useBreadcrumbs('overview'); + // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 2c6ed9d81744e..fd4e08f619495 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -64,8 +64,13 @@ export class IngestManagerPlugin IngestManagerStartDeps, IngestManagerStart ]; - const { renderApp } = await import('./applications/ingest_manager'); - return renderApp(coreStart, params, deps, startDeps, config); + const { renderApp, teardownIngestManager } = await import('./applications/ingest_manager'); + const unmount = renderApp(coreStart, params, deps, startDeps, config); + + return () => { + unmount(); + teardownIngestManager(coreStart); + }; }, }); } diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 80a33c26d86da..666d46f030780 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -136,12 +136,13 @@ export const getListHandler: RequestHandler = async (context, request, response) dashboards: enhancedDashboards, }; } + return { index: indexName, dataset: datasetBuckets.length ? datasetBuckets[0].key : '', namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', type: typeBuckets.length ? typeBuckets[0].key : '', - package: pkg, + package: pkgSavedObject.length ? pkg : '', package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', last_activity: lastActivity, size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index d86a3180f64d9..a8b22b3e22750 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -9,10 +9,9 @@ "expressions", "navigation", "kibanaLegacy", - "uiActions", "visualizations", "dashboard" ], - "optionalPlugins": ["embeddable", "usageCollection", "taskManager"], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"], "configPath": ["xpack", "lens"] } diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx index 6d5b1153ad1bc..5407389c7fc4c 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.test.tsx @@ -13,7 +13,7 @@ import { DatatableProps } from './expression'; import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { IFieldFormat } from '../../../../../src/plugins/data/public'; import { IAggType } from 'src/plugins/data/public'; -const executeTriggerActions = jest.fn(); +const onClickValue = jest.fn(); function sampleArgs() { const data: LensMultiTable = { @@ -66,7 +66,7 @@ describe('datatable_expression', () => { data={data} args={args} formatFactory={x => x as IFieldFormat} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} getType={jest.fn()} /> ) @@ -87,7 +87,7 @@ describe('datatable_expression', () => { }} args={args} formatFactory={x => x as IFieldFormat} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} /> ); @@ -97,18 +97,16 @@ describe('datatable_expression', () => { .first() .simulate('click'); - expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { - data: { - data: [ - { - column: 0, - row: 0, - table: data.tables.l1, - value: 10110, - }, - ], - negate: true, - }, + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 0, + row: 0, + table: data.tables.l1, + value: 10110, + }, + ], + negate: true, timeFieldName: undefined, }); }); @@ -127,7 +125,7 @@ describe('datatable_expression', () => { }} args={args} formatFactory={x => x as IFieldFormat} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} getType={jest.fn(() => ({ type: 'buckets' } as IAggType))} /> ); @@ -137,18 +135,16 @@ describe('datatable_expression', () => { .at(3) .simulate('click'); - expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { - data: { - data: [ - { - column: 1, - row: 0, - table: data.tables.l1, - value: 1588024800000, - }, - ], - negate: false, - }, + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 1, + row: 0, + table: data.tables.l1, + value: 1588024800000, + }, + ], + negate: false, timeFieldName: 'b', }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 71d29be1744bb..3be5c72d2af37 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -10,17 +10,17 @@ import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { IAggType } from 'src/plugins/data/public'; -import { FormatFactory, LensMultiTable } from '../types'; +import { + FormatFactory, + ILensInterpreterRenderHandlers, + LensFilterEvent, + LensMultiTable, +} from '../types'; import { ExpressionFunctionDefinition, ExpressionRenderDefinition, - IInterpreterRenderHandlers, } from '../../../../../src/plugins/expressions/public'; import { VisualizationContainer } from '../visualization_container'; -import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { getExecuteTriggerActions } from '../services'; export interface DatatableColumns { columnIds: string[]; } @@ -37,7 +37,7 @@ export interface DatatableProps { type DatatableRenderProps = DatatableProps & { formatFactory: FormatFactory; - executeTriggerActions: UiActionsStart['executeTriggerActions']; + onClickValue: (data: LensFilterEvent['data']) => void; getType: (name: string) => IAggType; }; @@ -125,17 +125,19 @@ export const getDatatableRenderer = (dependencies: { render: async ( domNode: Element, config: DatatableProps, - handlers: IInterpreterRenderHandlers + handlers: ILensInterpreterRenderHandlers ) => { const resolvedFormatFactory = await dependencies.formatFactory; - const executeTriggerActions = getExecuteTriggerActions(); const resolvedGetType = await dependencies.getType; + const onClickValue = (data: LensFilterEvent['data']) => { + handlers.event({ name: 'filter', data }); + }; ReactDOM.render( , @@ -162,21 +164,19 @@ export function DatatableComponent(props: DatatableRenderProps) { const timeFieldName = negate && isDateHistogram ? undefined : col?.meta?.aggConfigParams?.field; const rowIndex = firstTable.rows.findIndex(row => row[field] === value); - const context: ValueClickTriggerContext = { - data: { - negate, - data: [ - { - row: rowIndex, - column: colIndex, - value, - table: firstTable, - }, - ], - }, + const data: LensFilterEvent['data'] = { + negate, + data: [ + { + row: rowIndex, + column: colIndex, + value, + table: firstTable, + }, + ], timeFieldName, }; - props.executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); + props.onClickValue(data); }; return ( diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index 44894d31da51d..5cc3c40591c3f 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, CoreStart } from 'kibana/public'; +import { CoreSetup } from 'kibana/public'; import { datatableVisualization } from './visualization'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { datatable, datatableColumns, getDatatableRenderer } from './expression'; import { EditorFrameSetup, FormatFactory } from '../types'; -import { setExecuteTriggerActions } from '../services'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; @@ -42,7 +41,4 @@ export class DatatableVisualization { ); editorFrame.registerVisualization(datatableVisualization); } - start(core: CoreStart, { uiActions }: DatatableVisualizationPluginStartPlugins) { - setExecuteTriggerActions(uiActions.executeTriggerActions); - } } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index 78b038cf702f8..f9c5668ca1e06 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -21,6 +21,9 @@ import { import { ReactExpressionRendererType } from 'src/plugins/expressions/public'; import { DragDrop } from '../../drag_drop'; import { FrameLayout } from './frame_layout'; +import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -48,6 +51,11 @@ function getDefaultProps() { query: { query: '', language: 'lucene' }, filters: [], core: coreMock.createSetup(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + expressions: expressionsPluginMock.createStartContract(), + }, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 6da9a94711081..06d417ad18d54 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -26,6 +26,7 @@ import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; +import { EditorFrameStartPlugins } from '../service'; export interface EditorFrameProps { doc?: Document; @@ -36,6 +37,7 @@ export interface EditorFrameProps { ExpressionRenderer: ReactExpressionRendererType; onError: (e: { message: string }) => void; core: CoreSetup | CoreStart; + plugins: EditorFrameStartPlugins; dateRange: { fromDate: string; toDate: string; @@ -285,6 +287,7 @@ export function EditorFrame(props: EditorFrameProps) { dispatch={dispatch} ExpressionRenderer={props.ExpressionRenderer} core={props.core} + plugins={props.plugins} /> ) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 1f62929783b63..71aabaae3c65c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -9,6 +9,9 @@ import { EditorFrameProps } from './index'; import { Datasource, Visualization } from '../../types'; import { createExpressionRendererMock } from '../mocks'; import { coreMock } from 'src/core/public/mocks'; +import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; describe('editor_frame state management', () => { describe('initialization', () => { @@ -24,6 +27,11 @@ describe('editor_frame state management', () => { ExpressionRenderer: createExpressionRendererMock(), onChange: jest.fn(), core: coreMock.createSetup(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: dataPluginMock.createStartContract(), + expressions: expressionsPluginMock.createStartContract(), + }, dateRange: { fromDate: 'now-7d', toDate: 'now' }, query: { query: '', language: 'lucene' }, filters: [], diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx index 33ecee53fa3bc..a20626ebaaad7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.test.tsx @@ -22,6 +22,10 @@ import { DragDrop, ChildDragDropProvider } from '../../drag_drop'; import { Ast } from '@kbn/interpreter/common'; import { coreMock } from 'src/core/public/mocks'; import { esFilters, IFieldType, IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { TriggerId, UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; +import { TriggerContract } from '../../../../../../src/plugins/ui_actions/public/triggers'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; @@ -29,10 +33,15 @@ describe('workspace_panel', () => { let mockDatasource: DatasourceMock; let expressionRendererMock: jest.Mock; + let uiActionsMock: jest.Mocked; + let trigger: jest.Mocked>; let instance: ReactWrapper; beforeEach(() => { + trigger = ({ exec: jest.fn() } as unknown) as jest.Mocked>; + uiActionsMock = uiActionsPluginMock.createStartContract(); + uiActionsMock.getTrigger.mockReturnValue(trigger); mockVisualization = createMockVisualization(); mockVisualization2 = createMockVisualization(); @@ -60,6 +69,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -82,6 +92,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -104,6 +115,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -140,6 +152,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -198,6 +211,48 @@ describe('workspace_panel', () => { `); }); + it('should execute a trigger on expression event', () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} + /> + ); + + const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; + + const eventData = {}; + onEvent({ name: 'brush', data: eventData }); + + expect(uiActionsMock.getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); + expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); + }); + it('should include data fetching for each layer in the expression', () => { const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); @@ -237,6 +292,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -316,6 +372,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); }); @@ -370,6 +427,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); }); @@ -424,6 +482,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); @@ -461,6 +520,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); }); @@ -504,6 +564,7 @@ describe('workspace_panel', () => { dispatch={() => {}} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); }); @@ -559,6 +620,7 @@ describe('workspace_panel', () => { dispatch={mockDispatch} ExpressionRenderer={expressionRendererMock} core={coreMock.createSetup()} + plugins={{ uiActions: uiActionsMock }} /> ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx index e246d8e27a708..b000fc7fa0176 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel.tsx @@ -17,14 +17,25 @@ import { EuiButtonEmpty, } from '@elastic/eui'; import { CoreStart, CoreSetup } from 'kibana/public'; -import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { + ExpressionRendererEvent, + ReactExpressionRendererType, +} from '../../../../../../src/plugins/expressions/public'; import { Action } from './state_management'; -import { Datasource, Visualization, FramePublicAPI } from '../../types'; +import { + Datasource, + Visualization, + FramePublicAPI, + isLensBrushEvent, + isLensFilterEvent, +} from '../../types'; import { DragDrop, DragContext } from '../../drag_drop'; import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; import { buildExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -43,6 +54,7 @@ export interface WorkspacePanelProps { dispatch: (action: Action) => void; ExpressionRenderer: ReactExpressionRendererType; core: CoreStart | CoreSetup; + plugins: { uiActions?: UiActionsStart }; } export const WorkspacePanel = debouncedComponent(InnerWorkspacePanel); @@ -58,6 +70,7 @@ export function InnerWorkspacePanel({ framePublicAPI, dispatch, core, + plugins, ExpressionRenderer: ExpressionRendererComponent, }: WorkspacePanelProps) { const IS_DARK_THEME = core.uiSettings.get('theme:darkMode'); @@ -211,6 +224,22 @@ export function InnerWorkspacePanel({ className="lnsExpressionRenderer__component" padding="m" expression={expression!} + onEvent={(event: ExpressionRendererEvent) => { + if (!plugins.uiActions) { + // ui actions not available, not handling event... + return; + } + if (isLensBrushEvent(event)) { + plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: event.data, + }); + } + if (isLensFilterEvent(event)) { + plugins.uiActions.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: event.data, + }); + } + }} renderError={(errorMessage?: string | null) => { return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 8d95540b3e8b5..4e5b32ad7f7a3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -10,6 +10,7 @@ import { ReactExpressionRendererProps } from 'src/plugins/expressions/public'; import { Query, TimeRange, Filter, TimefilterContract } from 'src/plugins/data/public'; import { Document } from '../../persistence'; import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; jest.mock('../../../../../../src/plugins/inspector/public/', () => ({ isAvailable: false, @@ -34,10 +35,14 @@ const savedVis: Document = { describe('embeddable', () => { let mountpoint: HTMLDivElement; let expressionRenderer: jest.Mock; + let getTrigger: jest.Mock; + let trigger: { exec: jest.Mock }; beforeEach(() => { mountpoint = document.createElement('div'); expressionRenderer = jest.fn(_props => null); + trigger = { exec: jest.fn() }; + getTrigger = jest.fn(() => trigger); }); afterEach(() => { @@ -48,6 +53,7 @@ describe('embeddable', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, + getTrigger, { editPath: '', editUrl: '', @@ -70,6 +76,7 @@ describe('embeddable', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, + getTrigger, { editPath: '', editUrl: '', @@ -97,6 +104,7 @@ describe('embeddable', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, + getTrigger, { editPath: '', editUrl: '', @@ -114,6 +122,32 @@ describe('embeddable', () => { }); }); + it('should execute trigger on event from expression renderer', () => { + const embeddable = new Embeddable( + dataPluginMock.createSetupContract().query.timefilter.timefilter, + expressionRenderer, + getTrigger, + { + editPath: '', + editUrl: '', + editable: true, + savedVis, + }, + { id: '123' } + ); + embeddable.render(mountpoint); + + const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; + + const eventData = {}; + onEvent({ name: 'brush', data: eventData }); + + expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush); + expect(trigger.exec).toHaveBeenCalledWith( + expect.objectContaining({ data: eventData, embeddable: expect.anything() }) + ); + }); + it('should not re-render if only change is in disabled filter', () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; @@ -122,6 +156,7 @@ describe('embeddable', () => { const embeddable = new Embeddable( dataPluginMock.createSetupContract().query.timefilter.timefilter, expressionRenderer, + getTrigger, { editPath: '', editUrl: '', @@ -154,6 +189,7 @@ describe('embeddable', () => { const embeddable = new Embeddable( timefilter, expressionRenderer, + getTrigger, { editPath: '', editUrl: '', diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 559854cbab39a..796cf5b32e3ba 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -8,25 +8,30 @@ import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { - Query, - TimeRange, Filter, IIndexPattern, + Query, TimefilterContract, + TimeRange, } from 'src/plugins/data/public'; import { Subscription } from 'rxjs'; -import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; +import { + ExpressionRendererEvent, + ReactExpressionRendererType, +} from '../../../../../../src/plugins/expressions/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public'; import { Embeddable as AbstractEmbeddable, + EmbeddableInput, EmbeddableOutput, IContainer, - EmbeddableInput, } from '../../../../../../src/plugins/embeddable/public'; -import { Document, DOC_TYPE } from '../../persistence'; +import { DOC_TYPE, Document } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; +import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; +import { isLensBrushEvent, isLensFilterEvent } from '../../types'; export interface LensEmbeddableConfiguration { savedVis: Document; @@ -50,6 +55,7 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); @@ -100,6 +108,9 @@ export class Embeddable extends AbstractEmbeddable, domNode ); } + handleEvent = (event: ExpressionRendererEvent) => { + if (!this.getTrigger || this.input.disableTriggers) { + return; + } + if (isLensBrushEvent(event)) { + this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: event.data, + embeddable: this, + }); + } + if (isLensFilterEvent(event)) { + this.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec({ + data: event.data, + embeddable: this, + }); + } + }; + destroy() { super.destroy(); if (this.domNode) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 852d377915856..c23d44aa8e4b6 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -26,6 +26,7 @@ import { import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; import { getEditPath } from '../../../common'; +import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; interface StartServices { timefilter: TimefilterContract; @@ -34,6 +35,7 @@ interface StartServices { savedObjectsClient: SavedObjectsClientContract; expressionRenderer: ReactExpressionRendererType; indexPatternService: IndexPatternsContract; + uiActions?: UiActionsStart; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -74,6 +76,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { indexPatternService, timefilter, expressionRenderer, + uiActions, } = await this.getStartServices(); const store = new SavedObjectIndexStore(savedObjectsClient); const savedVis = await store.load(savedObjectId); @@ -99,6 +102,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { return new Embeddable( timefilter, expressionRenderer, + uiActions?.getTrigger, { savedVis, editPath: getEditPath(savedObjectId), diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 49c91affe3dc4..41706121830cb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -9,7 +9,10 @@ import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; import { TimeRange, Filter, Query } from 'src/plugins/data/public'; -import { ReactExpressionRendererType } from 'src/plugins/expressions/public'; +import { + ExpressionRendererEvent, + ReactExpressionRendererType, +} from 'src/plugins/expressions/public'; export interface ExpressionWrapperProps { ExpressionRenderer: ReactExpressionRendererType; @@ -20,12 +23,14 @@ export interface ExpressionWrapperProps { filters?: Filter[]; lastReloadRequestTime?: number; }; + handleEvent: (event: ExpressionRendererEvent) => void; } export function ExpressionWrapper({ ExpressionRenderer: ExpressionRendererComponent, expression, context, + handleEvent, }: ExpressionWrapperProps) { return ( @@ -51,6 +56,7 @@ export function ExpressionWrapper({ expression={expression} searchContext={{ ...context }} renderError={error =>
{error}
} + onEvent={handleEvent} /> )} diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index 15fe449d6563b..a815e70c58629 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -26,6 +26,7 @@ import { mergeTables } from './merge_tables'; import { formatColumn } from './format_column'; import { EmbeddableFactory } from './embeddable/embeddable_factory'; import { getActiveDatasourceIdFromDoc } from './editor_frame/state_management'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface EditorFrameSetupPlugins { data: DataPublicPluginSetup; @@ -37,6 +38,7 @@ export interface EditorFrameStartPlugins { data: DataPublicPluginStart; embeddable?: EmbeddableStart; expressions: ExpressionsStart; + uiActions?: UiActionsStart; } async function collectAsyncDefinitions( @@ -73,6 +75,7 @@ export class EditorFrameService { timefilter: deps.data.query.timefilter.timefilter, expressionRenderer: deps.expressions.ReactExpressionRenderer, indexPatternService: deps.data.indexPatterns, + uiActions: deps.uiActions, }; }; @@ -116,6 +119,7 @@ export class EditorFrameService { (doc && doc.visualizationType) || firstVisualizationId || null } core={core} + plugins={plugins} ExpressionRenderer={plugins.expressions.ReactExpressionRenderer} doc={doc} dateRange={dateRange} diff --git a/x-pack/plugins/lens/public/pie_visualization/index.ts b/x-pack/plugins/lens/public/pie_visualization/index.ts index b2aae2e8529a5..dd828c6c35300 100644 --- a/x-pack/plugins/lens/public/pie_visualization/index.ts +++ b/x-pack/plugins/lens/public/pie_visualization/index.ts @@ -5,13 +5,12 @@ */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { CoreSetup, CoreStart } from 'src/core/public'; +import { CoreSetup } from 'src/core/public'; import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { pieVisualization } from './pie_visualization'; import { pie, getPieRenderer } from './register_expression'; import { EditorFrameSetup, FormatFactory } from '../types'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { setExecuteTriggerActions } from '../services'; export interface PieVisualizationPluginSetupPlugins { editorFrame: EditorFrameSetup; @@ -44,10 +43,4 @@ export class PieVisualization { editorFrame.registerVisualization(pieVisualization); } - - start(core: CoreStart, { uiActions }: PieVisualizationPluginStartPlugins) { - setExecuteTriggerActions(uiActions.executeTriggerActions); - } - - stop() {} } diff --git a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx index 7babf7ed7ff46..bbc6a1dc75c3a 100644 --- a/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/register_expression.tsx @@ -14,9 +14,8 @@ import { ExpressionRenderDefinition, ExpressionFunctionDefinition, } from 'src/plugins/expressions/public'; -import { LensMultiTable, FormatFactory } from '../types'; +import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types'; import { PieExpressionProps, PieExpressionArgs } from './types'; -import { getExecuteTriggerActions } from '../services'; import { PieComponent } from './render_function'; export interface PieRender { @@ -109,7 +108,9 @@ export const getPieRenderer = (dependencies: { config: PieExpressionProps, handlers: IInterpreterRenderHandlers ) => { - const executeTriggerActions = getExecuteTriggerActions(); + const onClickValue = (data: LensFilterEvent['data']) => { + handlers.event({ name: 'filter', data }); + }; const formatFactory = await dependencies.formatFactory; ReactDOM.render( @@ -117,7 +118,7 @@ export const getPieRenderer = (dependencies: { {...config} {...dependencies} formatFactory={formatFactory} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} isDarkMode={dependencies.isDarkMode} /> , diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx index b0d4e0d2cc52b..a914efcead005 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Settings } from '@elastic/charts'; +import { SeriesIdentifier, Settings } from '@elastic/charts'; import { shallow } from 'enzyme'; import { LensMultiTable } from '../types'; import { PieComponent } from './render_function'; @@ -59,7 +59,7 @@ describe('PieVisualization component', () => { formatFactory: getFormatSpy, isDarkMode: false, chartTheme: {}, - executeTriggerActions: jest.fn(), + onClickValue: jest.fn(), }; } @@ -111,6 +111,58 @@ describe('PieVisualization component', () => { expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined(); }); + test('it calls filter callback with the given context', () => { + const defaultArgs = getDefaultArgs(); + const component = shallow(); + component + .find(Settings) + .first() + .prop('onElementClick')!([[[{ groupByRollup: 6, value: 6 }], {} as SeriesIdentifier]]); + + expect(defaultArgs.onClickValue.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "data": Array [ + Object { + "column": 0, + "row": 0, + "table": Object { + "columns": Array [ + Object { + "id": "a", + "name": "a", + }, + Object { + "id": "b", + "name": "b", + }, + Object { + "id": "c", + "name": "c", + }, + ], + "rows": Array [ + Object { + "a": 6, + "b": 2, + "c": "I", + "d": "Row 1", + }, + Object { + "a": 1, + "b": 5, + "c": "J", + "d": "Row 2", + }, + ], + "type": "kibana_datatable", + }, + "value": 6, + }, + ], + } + `); + }); + test('it shows emptyPlaceholder for undefined grouped data', () => { const defaultData = getDefaultArgs().data; const emptyData: LensMultiTable = { diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 56019b3e6c891..d812803272f3e 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -24,12 +24,10 @@ import { RecursivePartial, LayerValue, } from '@elastic/charts'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; -import { FormatFactory } from '../types'; +import { FormatFactory, LensFilterEvent } from '../types'; import { VisualizationContainer } from '../visualization_container'; import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants'; import { ColumnGroups, PieExpressionProps } from './types'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { getSliceValueWithFallback, getFilterContext } from './render_helpers'; import { EmptyPlaceholder } from '../shared_components'; import './visualization.scss'; @@ -43,13 +41,13 @@ export function PieComponent( formatFactory: FormatFactory; chartTheme: Exclude; isDarkMode: boolean; - executeTriggerActions: UiActionsStart['executeTriggerActions']; + onClickValue: (data: LensFilterEvent['data']) => void; } ) { const [firstTable] = Object.values(props.data.tables); const formatters: Record> = {}; - const { chartTheme, isDarkMode, executeTriggerActions } = props; + const { chartTheme, isDarkMode, onClickValue } = props; const { shape, groups, @@ -246,7 +244,7 @@ export function PieComponent( firstTable ); - executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); + onClickValue(context); }} /> { ], }; expect(getFilterContext([{ groupByRollup: 'Test', value: 100 }], ['a'], table)).toEqual({ - data: { - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }, + data: [ + { + row: 1, + column: 0, + value: 'Test', + table, + }, + ], }); }); @@ -124,16 +122,14 @@ describe('render helpers', () => { ], }; expect(getFilterContext([{ groupByRollup: 'Test', value: 100 }], ['a', 'b'], table)).toEqual({ - data: { - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - ], - }, + data: [ + { + row: 1, + column: 0, + value: 'Test', + table, + }, + ], }); }); @@ -161,22 +157,20 @@ describe('render helpers', () => { table ) ).toEqual({ - data: { - data: [ - { - row: 1, - column: 0, - value: 'Test', - table, - }, - { - row: 1, - column: 1, - value: 'Two', - table, - }, - ], - }, + data: [ + { + row: 1, + column: 0, + value: 'Test', + table, + }, + { + row: 1, + column: 1, + value: 'Two', + table, + }, + ], }); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index bc3c29ba0fff1..3f7494661c049 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -6,8 +6,8 @@ import { Datum, LayerValue } from '@elastic/charts'; import { KibanaDatatable, KibanaDatatableColumn } from 'src/plugins/expressions/public'; -import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; import { ColumnGroups } from './types'; +import { LensFilterEvent } from '../types'; export function getSliceValueWithFallback( d: Datum, @@ -28,7 +28,7 @@ export function getFilterContext( clickedLayers: LayerValue[], layerColumnIds: string[], table: KibanaDatatable -): ValueClickTriggerContext { +): LensFilterEvent['data'] { const matchingIndex = table.rows.findIndex(row => clickedLayers.every((layer, index) => { const columnId = layerColumnIds[index]; @@ -37,13 +37,11 @@ export function getFilterContext( ); return { - data: { - data: clickedLayers.map((clickedLayer, index) => ({ - column: table.columns.findIndex(col => col.id === layerColumnIds[index]), - row: matchingIndex, - value: clickedLayer.groupByRollup, - table, - })), - }, + data: clickedLayers.map((clickedLayer, index) => ({ + column: table.columns.findIndex(col => col.id === layerColumnIds[index]), + row: matchingIndex, + value: clickedLayer.groupByRollup, + table, + })), }; } diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index b2309657967f1..f9a577e001c64 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -103,9 +103,6 @@ export class LensPlugin { start(core: CoreStart, startDependencies: LensPluginStartDependencies) { this.createEditorFrame = this.editorFrameService.start(core, startDependencies).createInstance; - this.xyVisualization.start(core, startDependencies); - this.datatableVisualization.start(core, startDependencies); - this.pieVisualization.start(core, startDependencies); } stop() { diff --git a/x-pack/plugins/lens/public/services.ts b/x-pack/plugins/lens/public/services.ts deleted file mode 100644 index a66743dde2661..0000000000000 --- a/x-pack/plugins/lens/public/services.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { createGetterSetter } from '../../../../src/plugins/kibana_utils/public'; -import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; - -export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter< - UiActionsStart['executeTriggerActions'] ->('executeTriggerActions'); diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 04efc642793b0..42dcce0e438d7 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -7,11 +7,21 @@ import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; -import { KibanaDatatable, SerializedFieldFormat } from '../../../../src/plugins/expressions/public'; +import { + ExpressionRendererEvent, + IInterpreterRenderHandlers, + KibanaDatatable, + SerializedFieldFormat, +} from '../../../../src/plugins/expressions/public'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../common'; import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; +import { + SELECT_RANGE_TRIGGER, + TriggerContext, + VALUE_CLICK_TRIGGER, +} from '../../../../src/plugins/ui_actions/public'; export type ErrorCallback = (e: { message: string }) => void; @@ -467,3 +477,29 @@ export interface Visualization { */ toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; } + +export interface LensFilterEvent { + name: 'filter'; + data: TriggerContext['data']; +} +export interface LensBrushEvent { + name: 'brush'; + data: TriggerContext['data']; +} + +export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent { + return event.name === 'filter'; +} + +export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensBrushEvent { + return event.name === 'brush'; +} + +/** + * Expression renderer handlers specifically for lens renderers. This is a narrowed down + * version of the general render handlers, specifying supported event types. If this type is + * used, dispatched events will be handled correctly. + */ +export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers { + event: (event: LensFilterEvent | LensBrushEvent) => void; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts index 9a0819d4f01c4..23cf9e7ff818f 100644 --- a/x-pack/plugins/lens/public/xy_visualization/index.ts +++ b/x-pack/plugins/lens/public/xy_visualization/index.ts @@ -5,15 +5,13 @@ */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { CoreSetup, IUiSettingsClient, CoreStart } from 'kibana/public'; +import { CoreSetup, IUiSettingsClient } from 'kibana/public'; import moment from 'moment-timezone'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { xyVisualization } from './xy_visualization'; import { xyChart, getXyChartRenderer } from './xy_expression'; import { legendConfig, xConfig, layerConfig } from './types'; import { EditorFrameSetup, FormatFactory } from '../types'; -import { setExecuteTriggerActions } from '../services'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; export interface XyVisualizationPluginSetupPlugins { expressions: ExpressionsSetup; @@ -21,10 +19,6 @@ export interface XyVisualizationPluginSetupPlugins { editorFrame: EditorFrameSetup; } -interface XyVisualizationPluginStartPlugins { - uiActions: UiActionsStart; -} - function getTimeZone(uiSettings: IUiSettingsClient) { const configuredTimeZone = uiSettings.get('dateFormat:tz'); if (configuredTimeZone === 'Browser') { @@ -59,7 +53,4 @@ export class XyVisualization { editorFrame.registerVisualization(xyVisualization); } - start(core: CoreStart, { uiActions }: XyVisualizationPluginStartPlugins) { - setExecuteTriggerActions(uiActions.executeTriggerActions); - } } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 0f9aa1c10e127..72e51b175543c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -25,7 +25,8 @@ import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './ty import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -const executeTriggerActions = jest.fn(); +const onClickValue = jest.fn(); +const onSelectRange = jest.fn(); const dateHistogramData: LensMultiTable = { type: 'lens_multitable', @@ -296,7 +297,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -344,7 +346,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(` @@ -379,7 +382,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -415,7 +419,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -452,7 +457,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -496,7 +502,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -530,7 +537,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(Settings).prop('xDomain')).toBeUndefined(); @@ -546,7 +554,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -563,7 +572,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -580,7 +590,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -602,7 +613,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -611,12 +623,10 @@ describe('xy_expression', () => { .first() .prop('onBrushEnd')!({ x: [1585757732783, 1585758880838] }); - expect(executeTriggerActions).toHaveBeenCalledWith('SELECT_RANGE_TRIGGER', { - data: { - column: 0, - table: dateHistogramData.tables.timeLayer, - range: [1585757732783, 1585758880838], - }, + expect(onSelectRange).toHaveBeenCalledWith({ + column: 0, + table: dateHistogramData.tables.timeLayer, + range: [1585757732783, 1585758880838], timeFieldName: 'order_date', }); }); @@ -656,7 +666,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -665,23 +676,21 @@ describe('xy_expression', () => { .first() .prop('onElementClick')!([[geometry, series as XYChartSeriesIdentifier]]); - expect(executeTriggerActions).toHaveBeenCalledWith('VALUE_CLICK_TRIGGER', { - data: { - data: [ - { - column: 1, - row: 1, - table: data.tables.first, - value: 5, - }, - { - column: 1, - row: 0, - table: data.tables.first, - value: 2, - }, - ], - }, + expect(onClickValue).toHaveBeenCalledWith({ + data: [ + { + column: 1, + row: 1, + table: data.tables.first, + value: 5, + }, + { + column: 1, + row: 0, + table: data.tables.first, + value: 2, + }, + ], }); }); @@ -695,7 +704,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -713,7 +723,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -734,7 +745,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component).toMatchSnapshot(); @@ -753,7 +765,8 @@ describe('xy_expression', () => { timeZone="CEST" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(LineSeries).prop('timeZone')).toEqual('CEST'); @@ -771,7 +784,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -796,7 +810,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(true); @@ -815,7 +830,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(BarSeries).prop('enableHistogramMode')).toEqual(false); @@ -876,7 +892,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); }; @@ -1071,7 +1088,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(LineSeries).prop('xScaleType')).toEqual(ScaleType.Ordinal); @@ -1088,7 +1106,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(component.find(LineSeries).prop('yScaleType')).toEqual(ScaleType.Sqrt); @@ -1105,7 +1124,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -1123,7 +1143,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -1141,7 +1162,8 @@ describe('xy_expression', () => { chartTheme={{}} histogramBarTarget={50} timeZone="UTC" - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); expect(getFormatSpy).toHaveBeenCalledWith({ @@ -1161,7 +1183,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -1248,7 +1271,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); @@ -1302,7 +1326,8 @@ describe('xy_expression', () => { timeZone="UTC" chartTheme={{}} histogramBarTarget={50} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index d598b9c740655..cb2defbc54f49 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -21,24 +21,22 @@ import { } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import { - IInterpreterRenderHandlers, - ExpressionRenderDefinition, ExpressionFunctionDefinition, + ExpressionRenderDefinition, ExpressionValueSearchContext, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { - ValueClickTriggerContext, - RangeSelectTriggerContext, -} from '../../../../../src/plugins/embeddable/public'; -import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; -import { LensMultiTable, FormatFactory } from '../types'; + LensMultiTable, + FormatFactory, + ILensInterpreterRenderHandlers, + LensFilterEvent, + LensBrushEvent, +} from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; import { VisualizationContainer } from '../visualization_container'; import { isHorizontalChart } from './state_helpers'; -import { getExecuteTriggerActions } from '../services'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { parseInterval } from '../../../../../src/plugins/data/common'; import { EmptyPlaceholder } from '../shared_components'; @@ -63,7 +61,8 @@ type XYChartRenderProps = XYChartProps & { formatFactory: FormatFactory; timeZone: string; histogramBarTarget: number; - executeTriggerActions: UiActionsStart['executeTriggerActions']; + onClickValue: (data: LensFilterEvent['data']) => void; + onSelectRange: (data: LensBrushEvent['data']) => void; }; export const xyChart: ExpressionFunctionDefinition< @@ -125,9 +124,18 @@ export const getXyChartRenderer = (dependencies: { }), validate: () => undefined, reuseDomNode: true, - render: async (domNode: Element, config: XYChartProps, handlers: IInterpreterRenderHandlers) => { - const executeTriggerActions = getExecuteTriggerActions(); + render: async ( + domNode: Element, + config: XYChartProps, + handlers: ILensInterpreterRenderHandlers + ) => { handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + const onClickValue = (data: LensFilterEvent['data']) => { + handlers.event({ name: 'filter', data }); + }; + const onSelectRange = (data: LensBrushEvent['data']) => { + handlers.event({ name: 'brush', data }); + }; const formatFactory = await dependencies.formatFactory; ReactDOM.render( @@ -137,7 +145,8 @@ export const getXyChartRenderer = (dependencies: { chartTheme={dependencies.chartTheme} timeZone={dependencies.timeZone} histogramBarTarget={dependencies.histogramBarTarget} - executeTriggerActions={executeTriggerActions} + onClickValue={onClickValue} + onSelectRange={onSelectRange} /> , domNode, @@ -177,7 +186,8 @@ export function XYChart({ timeZone, chartTheme, histogramBarTarget, - executeTriggerActions, + onClickValue, + onSelectRange, }: XYChartRenderProps) { const { legend, layers } = args; @@ -287,15 +297,13 @@ export function XYChart({ ); const timeFieldName = table.columns[xAxisColumnIndex]?.meta?.aggConfigParams?.field; - const context: RangeSelectTriggerContext = { - data: { - range: [min, max], - table, - column: xAxisColumnIndex, - }, + const context: LensBrushEvent['data'] = { + range: [min, max], + table, + column: xAxisColumnIndex, timeFieldName, }; - executeTriggerActions(VIS_EVENT_TO_TRIGGER.brush, context); + onSelectRange(context); }} onElementClick={([[geometry, series]]) => { // for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue @@ -337,18 +345,16 @@ export function XYChart({ ?.aggConfigParams?.field; const timeFieldName = xDomain && xAxisFieldName; - const context: ValueClickTriggerContext = { - data: { - data: points.map(point => ({ - row: point.row, - column: point.column, - value: point.value, - table, - })), - }, + const context: LensFilterEvent['data'] = { + data: points.map(point => ({ + row: point.row, + column: point.column, + value: point.value, + table, + })), timeFieldName, }; - executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context); + onClickValue(context); }} /> diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 6c04f7c19ac7d..ccbc8a1c21324 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import turf from 'turf'; import React from 'react'; import { AbstractLayer } from '../layer'; import { VectorStyle } from '../../styles/vector/vector_style'; @@ -30,6 +29,7 @@ import { canSkipFormattersUpdate, } from '../../util/can_skip_fetch'; import { assignFeatureIds } from '../../util/assign_feature_ids'; +import { getFeatureCollectionBounds } from '../../util/get_feature_collection_bounds'; import { getFillFilterExpression, getLineFilterExpression, @@ -153,31 +153,10 @@ export class VectorLayer extends AbstractLayer { return this.getCurrentStyle().renderLegendDetails(); } - _getBoundsBasedOnData() { - const featureCollection = this._getSourceFeatureCollection(); - if (!featureCollection) { - return null; - } - - const visibleFeatures = featureCollection.features.filter( - feature => feature.properties[FEATURE_VISIBLE_PROPERTY_NAME] - ); - const bbox = turf.bbox({ - type: 'FeatureCollection', - features: visibleFeatures, - }); - return { - minLon: bbox[0], - minLat: bbox[1], - maxLon: bbox[2], - maxLat: bbox[3], - }; - } - async getBounds(dataFilters) { const isStaticLayer = !this.getSource().isBoundsAware(); if (isStaticLayer) { - return this._getBoundsBasedOnData(); + return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this._hasJoins()); } const searchFilters = this._getSearchFilters( diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.test.tsx b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.test.tsx new file mode 100644 index 0000000000000..ad8b8d37f1310 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getFeatureCollectionBounds } from './get_feature_collection_bounds'; +import { FeatureCollection, Feature, Point } from 'geojson'; +import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +const visibleFeature: Feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [0, 0], + } as Point, + properties: { + [FEATURE_VISIBLE_PROPERTY_NAME]: true, + }, +}; + +const nonVisibleFeature: Feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [10, 0], + } as Point, + properties: { + [FEATURE_VISIBLE_PROPERTY_NAME]: false, + }, +}; + +const featureWithoutVisibilityProp: Feature = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-10, 0], + } as Point, + properties: {}, +}; + +const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: [visibleFeature, nonVisibleFeature, featureWithoutVisibilityProp], +}; + +test('should return bounding box for visible features with join', () => { + expect(getFeatureCollectionBounds(featureCollection, true)).toEqual({ + maxLat: 0, + maxLon: 0, + minLat: 0, + minLon: 0, + }); +}); + +test('should return bounding box for all features without join', () => { + expect(getFeatureCollectionBounds(featureCollection, false)).toEqual({ + maxLat: 0, + maxLon: 10, + minLat: 0, + minLon: -10, + }); +}); + +test('should return null when there are no features', () => { + const featureCollectionWithNoVisibileFeatures: FeatureCollection = { + type: 'FeatureCollection', + features: [nonVisibleFeature, featureWithoutVisibilityProp], + }; + expect(getFeatureCollectionBounds(featureCollectionWithNoVisibileFeatures, true)).toBeNull(); +}); diff --git a/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts new file mode 100644 index 0000000000000..4247233b295e1 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/util/get_feature_collection_bounds.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import turf from 'turf'; +import { FeatureCollection } from 'geojson'; +import { MapExtent } from '../../../common/descriptor_types'; +import { FEATURE_VISIBLE_PROPERTY_NAME } from '../../../common/constants'; + +export function getFeatureCollectionBounds( + featureCollection: FeatureCollection | null, + hasJoins: boolean +): MapExtent | null { + if (!featureCollection) { + return null; + } + + const visibleFeatures = hasJoins + ? featureCollection.features.filter(feature => { + return feature.properties && feature.properties[FEATURE_VISIBLE_PROPERTY_NAME]; + }) + : featureCollection.features; + + if (visibleFeatures.length === 0) { + return null; + } + + const bbox = turf.bbox({ + type: 'FeatureCollection', + features: visibleFeatures, + }); + return { + minLon: bbox[0], + minLat: bbox[1], + maxLon: bbox[2], + maxLat: bbox[3], + }; +} diff --git a/x-pack/plugins/ml/package.json b/x-pack/plugins/ml/package.json index 739dd806fcbb9..d69d6657fe68c 100644 --- a/x-pack/plugins/ml/package.json +++ b/x-pack/plugins/ml/package.json @@ -6,7 +6,7 @@ "license": "Elastic-License", "scripts": { "build:apiDocScripts": "cd server/routes/apidoc_scripts && tsc", - "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md" + "apiDocs": "yarn build:apiDocScripts && cd ./server/routes/ && apidoc --parse-workers apischema=./apidoc_scripts/target/schema_worker.js --parse-parsers apischema=./apidoc_scripts/target/schema_parser.js --parse-filters apiversion=./apidoc_scripts/target/version_filter.js -i . -o ../routes_doc && apidoc-markdown -p ../routes_doc -o ../routes_doc/ML_API.md -t ./apidoc_scripts/template.md" }, "devDependencies": { "apidoc": "^0.20.1", diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index cd311c285d0df..c2cb1ad9f0a57 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -1,7 +1,7 @@ { "name": "ml_kibana_api", "version": "7.8.0", - "description": "ML Kibana API", + "description": "This is the documentation of the REST API provided by the Machine Learning Kibana plugin. Each API is experimental and can include breaking changes in any version.", "title": "ML Kibana API", "order": [ "DataFrameAnalytics", diff --git a/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md new file mode 100644 index 0000000000000..70de461da18d8 --- /dev/null +++ b/x-pack/plugins/ml/server/routes/apidoc_scripts/template.md @@ -0,0 +1,143 @@ + +# <%= project.name %> v<%= project.version %> + +<%= project.description %> + +<% if (prepend) { -%> +<%- prepend %> +<% } -%> +<% data.forEach(group => { -%> + +## <%= group.name %> +<% group.subs.forEach(sub => { -%> + +### <%= sub.title %> +[Back to top](#top) + +<%- sub.description ? `${sub.description}\n\n` : '' -%> +``` +<%- sub.type.toUpperCase() %> <%= sub.url %> +``` +<% if (sub.header && sub.header.fields && sub.header.fields.Header.length) { -%> + +#### Headers +| Name | Type | Description | +|---------|-----------|--------------------------------------| +<% sub.header.fields.Header.forEach(header => { -%> +| <%- header.field %> | <%- header.type ? `\`${header.type}\`` : '' %> | <%- header.optional ? '**optional**' : '' %><%- header.description %> | +<% }) // foreach parameter -%> +<% } // if parameters -%> +<% if (sub.header && sub.header.examples && sub.header.examples.length) { -%> + +#### Header examples +<% sub.header.examples.forEach(example => { -%> +<%= example.title %> + +``` +<%- example.content %> +``` +<% }) // foreach example -%> +<% } // if example -%> +<% if (sub.parameter && sub.parameter.fields) { -%> +<% Object.keys(sub.parameter.fields).forEach(g => { -%> + +#### Parameters - `<%= g -%>` +| Name | Type | Description | +|:---------|:-----------|:--------------------------------------| +<% sub.parameter.fields[g].forEach(param => { -%> +| <%- param.field -%> | <%- param.type ? `\`${param.type}\`` : '' %> | <%- param.optional ? '**optional** ' : '' -%><%- param.description -%> +<% if (param.defaultValue) { -%> +_Default value: <%= param.defaultValue %>_
<% } -%> +<% if (param.size) { -%> +_Size range: <%- param.size %>_
<% } -%> +<% if (param.allowedValues) { -%> +_Allowed values: <%- param.allowedValues %>_<% } -%> | +<% }) // foreach (group) parameter -%> +<% }) // foreach param parameter -%> +<% } // if parameters -%> +<% if (sub.examples && sub.examples.length) { -%> + +#### Examples +<% sub.examples.forEach(example => { -%> +<%= example.title %> + +``` +<%- example.content %> +``` +<% }) // foreach example -%> +<% } // if example -%> +<% if (sub.parameter && sub.parameter.examples && sub.parameter.examples.length) { -%> + +#### Parameters examples +<% sub.parameter.examples.forEach(exampleParam => { -%> +`<%= exampleParam.type %>` - <%= exampleParam.title %> + +```<%= exampleParam.type %> +<%- exampleParam.content %> +``` +<% }) // foreach exampleParam -%> +<% } // if exampleParam -%> +<% if (sub.success && sub.success.fields) { -%> + +#### Success response +<% Object.keys(sub.success.fields).forEach(g => { -%> + +##### Success response - `<%= g %>` +| Name | Type | Description | +|:---------|:-----------|:--------------------------------------| +<% sub.success.fields[g].forEach(param => { -%> +| <%- param.field %> | <%- param.type ? `\`${param.type}\`` : '' %> | <%- param.optional ? '**optional**' : '' %><%- param.description -%> +<% if (param.defaultValue) { -%> +_Default value: <%- param.defaultValue %>_
<% } -%> +<% if (param.size) { -%> +_Size range: <%- param.size -%>_
<% } -%> +<% if (param.allowedValues) { -%> +_Allowed values: <%- param.allowedValues %>_<% } -%> | +<% }) // foreach (group) parameter -%> +<% }) // foreach field -%> +<% } // if success.fields -%> +<% if (sub.success && sub.success.examples && sub.success.examples.length) { -%> + +#### Success response example +<% sub.success.examples.forEach(example => { -%> + +##### Success response example - `<%= example.title %>` + +``` +<%- example.content %> +``` +<% }) // foreach success example -%> +<% } // if success.examples -%> +<% if (sub.error && sub.error.fields) { -%> + +#### Error response +<% Object.keys(sub.error.fields).forEach(g => { -%> + +##### Error response - `<%= g %>` +| Name | Type | Description | +|:---------|:-----------|:--------------------------------------| +<% sub.error.fields[g].forEach(param => { -%> +| <%- param.field %> | <%- param.type ? `\`${param.type}\`` : '' %> | <%- param.optional ? '**optional**' : '' %><%- param.description -%> +<% if (param.defaultValue) { -%> +_Default value: <%- param.defaultValue %>_
<% } -%> +<% if (param.size) { -%> +_Size range: <%- param.size -%>_
<% } -%> +<% if (param.allowedValues) { -%> +_Allowed values: <%- param.allowedValues %>_<% } -%> | +<% }) // foreach (group) parameter -%> +<% }) // foreach field -%> +<% } // if error.fields -%> +<% if (sub.error && sub.error.examples && sub.error.examples.length) { -%> + +#### Error response example +<% sub.error.examples.forEach(example => { -%> + +##### Error response example - `<%= example.title %>` + +``` +<%- example.content %> +``` +<% }) // foreach error example -%> +<% } // if error.examples -%> +<% }) // foreach sub -%> +<% }) // foreach group -%> diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_list.helpers.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_list.helpers.js index dc9b22b40542a..1d5bc52038ffc 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_list.helpers.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/helpers/remote_clusters_list.helpers.js @@ -69,6 +69,10 @@ export const setup = props => { remoteClusterLink.simulate('click'); }; + const clickPaginationNextButton = () => { + testBed.find('remoteClusterListTable.pagination-button-next').simulate('click'); + }; + return { ...testBed, actions: { @@ -77,6 +81,7 @@ export const setup = props => { clickRowActionButtonAt, clickConfirmModalDeleteRemoteCluster, clickRemoteClusterAt, + clickPaginationNextButton, }, }; }; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index bc73387831c9d..44b28eb9e783e 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -69,6 +69,53 @@ describe('', () => { }); }); + describe('when there are multiple pages of remote clusters', () => { + let find; + let table; + let actions; + let waitFor; + let form; + + const remoteClusters = [ + { + name: 'unique', + seeds: [], + }, + ]; + + for (let i = 0; i < 29; i++) { + remoteClusters.push({ + name: `name${i}`, + seeds: [], + }); + } + + beforeEach(async () => { + httpRequestsMockHelpers.setLoadRemoteClustersResponse(remoteClusters); + + await act(async () => { + ({ find, table, actions, waitFor, form } = setup()); + await waitFor('remoteClusterListTable'); + }); + }); + + test('pagination works', () => { + actions.clickPaginationNextButton(); + const { tableCellsValues } = table.getMetaData('remoteClusterListTable'); + + // Pagination defaults to 20 remote clusters per page. We loaded 30 remote clusters, + // so the second page should have 10. + expect(tableCellsValues.length).toBe(10); + }); + + // Skipped until we can figure out how to get this test to work. + test.skip('search works', () => { + form.setInputValue(find('remoteClusterSearch'), 'unique'); + const { tableCellsValues } = table.getMetaData('remoteClusterListTable'); + expect(tableCellsValues.length).toBe(1); + }); + }); + describe('when there are remote clusters', () => { let find; let exists; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js index 73f32fe8bca5b..739c6e26784ef 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/remote_cluster_table/remote_cluster_table.js @@ -25,6 +25,24 @@ import { PROXY_MODE } from '../../../../../common/constants'; import { getRouterLinkProps, trackUiMetric, METRIC_TYPE } from '../../../services'; import { ConnectionStatus, RemoveClusterButtonProvider } from '../components'; +const getFilteredClusters = (clusters, queryText) => { + if (queryText) { + const normalizedSearchText = queryText.toLowerCase(); + + return clusters.filter(cluster => { + const { name, seeds } = cluster; + const normalizedName = name.toLowerCase(); + if (normalizedName.toLowerCase().includes(normalizedSearchText)) { + return true; + } + + return seeds.some(seed => seed.includes(normalizedSearchText)); + }); + } else { + return clusters; + } +}; + export class RemoteClusterTable extends Component { static propTypes = { clusters: PropTypes.array, @@ -35,46 +53,47 @@ export class RemoteClusterTable extends Component { clusters: [], }; + static getDerivedStateFromProps(props, state) { + const { clusters } = props; + const { prevClusters, queryText } = state; + + // If a remote cluster gets deleted, we need to recreate the cached filtered clusters. + if (prevClusters !== clusters) { + return { + prevClusters: clusters, + filteredClusters: getFilteredClusters(clusters, queryText), + }; + } + + return null; + } + constructor(props) { super(props); this.state = { - queryText: undefined, + prevClusters: props.clusters, selectedItems: [], + filteredClusters: props.clusters, + queryText: '', }; } onSearch = ({ query }) => { + const { clusters } = this.props; const { text } = query; - const normalizedSearchText = text.toLowerCase(); + + // We cache the filtered indices instead of calculating them inside render() because + // of https://github.com/elastic/eui/issues/3445. this.setState({ - queryText: normalizedSearchText, + queryText: text, + filteredClusters: getFilteredClusters(clusters, text), }); }; - getFilteredClusters = () => { - const { clusters } = this.props; - const { queryText } = this.state; - - if (queryText) { - return clusters.filter(cluster => { - const { name, seeds } = cluster; - const normalizedName = name.toLowerCase(); - if (normalizedName.toLowerCase().includes(queryText)) { - return true; - } - - return seeds.some(seed => seed.includes(queryText)); - }); - } else { - return clusters.slice(0); - } - }; - render() { const { openDetailPanel } = this.props; - - const { selectedItems } = this.state; + const { selectedItems, filteredClusters } = this.state; const columns = [ { @@ -314,6 +333,7 @@ export class RemoteClusterTable extends Component { onChange: this.onSearch, box: { incremental: true, + 'data-test-subj': 'remoteClusterSearch', }, }; @@ -327,8 +347,6 @@ export class RemoteClusterTable extends Component { selectable: ({ isConfiguredByNode }) => !isConfiguredByNode, }; - const filteredClusters = this.getFilteredClusters(); - return ( { {this.props.selector.providers.map(provider => ( - ), - pushCallouts: null, - })); + usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService })); + useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, isLoading: false })); }); it('should render CaseComponent', async () => { @@ -328,6 +328,7 @@ describe('CaseView ', () => { ...defaultUseGetCaseUserActions, hasDataToPush: true, })); + const wrapper = mount( @@ -335,20 +336,24 @@ describe('CaseView ', () => { ); + + await wait(); + expect( wrapper .find('[data-test-subj="has-data-to-push-button"]') .first() .exists() ).toBeTruthy(); + wrapper - .find('[data-test-subj="mock-button"]') + .find('[data-test-subj="push-to-external-service"]') .first() .simulate('click'); + wrapper.update(); - await wait(); - expect(updateCase).toBeCalledWith(caseProps.caseData); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + + expect(postPushToService).toHaveBeenCalled(); }); it('should return null if error', () => { @@ -429,4 +434,32 @@ describe('CaseView ', () => { expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); expect(fetchCase).toBeCalled(); }); + + it('should disable the push button when connector is invalid', () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: true, + })); + + const wrapper = mount( + + + + + + ); + + expect( + wrapper + .find('button[data-test-subj="push-to-external-service"]') + .first() + .prop('disabled') + ).toBeTruthy(); + }); }); diff --git a/x-pack/plugins/siem/public/cases/components/case_view/index.tsx b/x-pack/plugins/siem/public/cases/components/case_view/index.tsx index d02119580a75a..163272f5087d7 100644 --- a/x-pack/plugins/siem/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/case_view/index.tsx @@ -18,6 +18,7 @@ import styled from 'styled-components'; import * as i18n from './translations'; import { Case } from '../../containers/types'; import { getCaseUrl } from '../../../common/components/link_to'; +import { gutterTimeline } from '../../../common/lib/helpers'; import { HeaderPage } from '../../../common/components/header_page'; import { EditableTitle } from '../../../common/components/header_page/editable_title'; import { TagList } from '../tag_list'; @@ -26,9 +27,8 @@ import { UserActionTree } from '../user_action_tree'; import { UserList } from '../user_list'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { WrapperPage } from '../../../common/components/wrapper_page'; import { getTypedPayload } from '../../containers/utils'; -import { WhitePageWrapper } from '../wrappers'; +import { WhitePageWrapper, HeaderWrapper } from '../wrappers'; import { useBasePath } from '../../../common/lib/kibana'; import { CaseStatus } from '../case_status'; import { navTabs } from '../../../app/home/home_navigations'; @@ -43,8 +43,11 @@ interface Props { userCanCrud: boolean; } -const MyWrapper = styled(WrapperPage)` - padding-bottom: 0; +const MyWrapper = styled.div` + padding: ${({ + theme, + }) => `${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} + ${theme.eui.paddingSizes.l}`}; `; const MyEuiFlexGroup = styled(EuiFlexGroup)` @@ -160,10 +163,11 @@ export const CaseComponent = React.memo( ); const { loading: isLoadingConnectors, connectors } = useConnectors(); - const caseConnectorName = useMemo( - () => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none', - [connectors, caseData.connectorId] - ); + + const [caseConnectorName, isValidConnector] = useMemo(() => { + const connector = connectors.find(c => c.id === caseData.connectorId); + return [connector?.name ?? 'none', !!connector]; + }, [connectors, caseData.connectorId]); const currentExternalIncident = useMemo( () => @@ -182,6 +186,7 @@ export const CaseComponent = React.memo( connectors, updateCase: handleUpdateCase, userCanCrud, + isValidConnector, }); const onSubmitConnector = useCallback( @@ -242,15 +247,20 @@ export const CaseComponent = React.memo( } }, [initLoadingData, isLoadingUserActions]); + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + dataTestSubj: 'backToCases', + }), + [search] + ); + return ( <> - + ( {...caseStatusData} /> - + {!initLoadingData && pushCallouts != null && pushCallouts} diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx index cb00201942312..e3e627e3a136e 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx @@ -46,7 +46,9 @@ describe('usePushToService', () => { connectors: connectorsMock, updateCase, userCanCrud: true, + isValidConnector: true, }; + beforeEach(() => { jest.resetAllMocks(); (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); @@ -55,6 +57,7 @@ describe('usePushToService', () => { actionLicense, })); }); + it('push case button posts the push with correct args', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( @@ -75,6 +78,7 @@ describe('usePushToService', () => { expect(result.current.pushCallouts).toBeNull(); }); }); + it('Displays message when user does not have premium license', async () => { (useGetActionLicense as jest.Mock).mockImplementation(() => ({ isLoading: false, @@ -96,6 +100,7 @@ describe('usePushToService', () => { expect(errorsMsg[0].title).toEqual(getLicenseError().title); }); }); + it('Displays message when user does not have case enabled in config', async () => { (useGetActionLicense as jest.Mock).mockImplementation(() => ({ isLoading: false, @@ -117,6 +122,7 @@ describe('usePushToService', () => { expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); }); }); + it('Displays message when user does not have a connector configured', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( @@ -135,6 +141,27 @@ describe('usePushToService', () => { expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); }); }); + + it('Displays message when connector is deleted', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseConnectorId: 'not-exist', + isValidConnector: false, + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx index 157639f011fef..ae8a67b75d36c 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx @@ -29,6 +29,7 @@ export interface UsePushToService { connectors: Connector[]; updateCase: (newCase: Case) => void; userCanCrud: boolean; + isValidConnector: boolean; } export interface ReturnUsePushToService { @@ -45,6 +46,7 @@ export const usePushToService = ({ connectors, updateCase, userCanCrud, + isValidConnector, }: UsePushToService): ReturnUsePushToService => { const urlSearch = useGetUrlSearch(navTabs.case); @@ -77,7 +79,7 @@ export const usePushToService = ({ description: ( @@ -97,7 +99,20 @@ export const usePushToService = ({ description: ( + ), + }, + ]; + } else if (!isValidConnector && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + ), }, @@ -130,7 +145,9 @@ export const usePushToService = ({ fill iconType="importAction" onClick={handlePushToService} - disabled={isLoading || loadingLicense || errorsMsg.length > 0 || !userCanCrud} + disabled={ + isLoading || loadingLicense || errorsMsg.length > 0 || !userCanCrud || !isValidConnector + } isLoading={isLoading} > {caseServices[caseConnectorId] @@ -147,6 +164,7 @@ export const usePushToService = ({ isLoading, loadingLicense, userCanCrud, + isValidConnector, ]); const objToReturn = useMemo(() => { diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts b/x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts index bdd6ae98a5d01..4b55aa83ef726 100644 --- a/x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts @@ -15,9 +15,10 @@ export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate( export const PUSH_THIRD = (thirdParty: string) => { if (thirdParty === 'none') { return i18n.translate('xpack.siem.case.caseView.pushThirdPartyIncident', { - defaultMessage: 'Push as third party incident', + defaultMessage: 'Push as external incident', }); } + return i18n.translate('xpack.siem.case.caseView.pushNamedIncident', { values: { thirdParty }, defaultMessage: 'Push as { thirdParty } incident', @@ -27,9 +28,10 @@ export const PUSH_THIRD = (thirdParty: string) => { export const UPDATE_THIRD = (thirdParty: string) => { if (thirdParty === 'none') { return i18n.translate('xpack.siem.case.caseView.updateThirdPartyIncident', { - defaultMessage: 'Update third party incident', + defaultMessage: 'Update external incident', }); } + return i18n.translate('xpack.siem.case.caseView.updateNamedIncident', { values: { thirdParty }, defaultMessage: 'Update { thirdParty } incident', diff --git a/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx b/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx index 772d78f948b79..06715514e01bf 100644 --- a/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx +++ b/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; +import { gutterTimeline } from '../../../common/lib/helpers'; export const WhitePageWrapper = styled.div` - ${({ theme }) => css` - background-color: ${theme.eui.euiColorEmptyShade}; - border-top: ${theme.eui.euiBorderThin}; - height: 100%; - min-height: 100vh; - `} + background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; + border-top: ${({ theme }) => theme.eui.euiBorderThin}; + height: 100%; + min-height: 100vh; `; export const SectionWrapper = styled.div` @@ -20,3 +19,8 @@ export const SectionWrapper = styled.div` margin: 0 auto; max-width: 1175px; `; + +export const HeaderWrapper = styled.div` + padding: ${({ theme }) => `${theme.eui.paddingSizes.l} ${gutterTimeline} 0 + ${theme.eui.paddingSizes.l}`}; +`; diff --git a/x-pack/plugins/siem/public/cases/pages/case_details.tsx b/x-pack/plugins/siem/public/cases/pages/case_details.tsx index 5ea5e52951592..5dfe12179b990 100644 --- a/x-pack/plugins/siem/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/siem/public/cases/pages/case_details.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { useParams, Redirect } from 'react-router-dom'; +import { WrapperPage } from '../../common/components/wrapper_page'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; @@ -26,10 +27,15 @@ export const CaseDetailsPage = React.memo(() => { return caseId != null ? ( <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} - + + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + ) : null; diff --git a/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx b/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx index bea3a9fb110ab..f70ff859e8e7d 100644 --- a/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx @@ -6,6 +6,7 @@ import React, { useMemo } from 'react'; import { Redirect } from 'react-router-dom'; +import styled from 'styled-components'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; @@ -18,12 +19,6 @@ import { ConfigureCases } from '../components/configure_cases'; import { WhitePageWrapper, SectionWrapper } from '../components/wrappers'; import * as i18n from './translations'; -const wrapperPageStyle: Record = { - paddingLeft: '0', - paddingRight: '0', - paddingBottom: '0', -}; - const ConfigureCasesPageComponent: React.FC = () => { const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); @@ -40,11 +35,17 @@ const ConfigureCasesPageComponent: React.FC = () => { return ; } + const HeaderWrapper = styled.div` + padding-top: ${({ theme }) => theme.eui.paddingSizes.l}; + `; + return ( <> - + - + + + diff --git a/x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx index bac0357def942..049d18e59d8be 100644 --- a/x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx @@ -6,26 +6,26 @@ import classNames from 'classnames'; import React from 'react'; -import styled, { css } from 'styled-components'; +import styled from 'styled-components'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; -const Wrapper = styled.div` - ${({ theme }) => css` - padding: ${theme.eui.paddingSizes.l} ${gutterTimeline} ${theme.eui.paddingSizes.l} - ${theme.eui.paddingSizes.l}; - - &.siemWrapperPage--restrictWidthDefault, - &.siemWrapperPage--restrictWidthCustom { - box-sizing: content-box; - margin: 0 auto; - } +const Wrapper = styled.div<{ noPadding?: boolean }>` + padding: ${props => + props.noPadding + ? '0' + : `${props.theme.eui.paddingSizes.l} ${gutterTimeline} ${props.theme.eui.paddingSizes.l} + ${props.theme.eui.paddingSizes.l}`}; + &.siemWrapperPage--restrictWidthDefault, + &.siemWrapperPage--restrictWidthCustom { + box-sizing: content-box; + margin: 0 auto; + } - &.siemWrapperPage--restrictWidthDefault { - max-width: 1000px; - } - `} + &.siemWrapperPage--restrictWidthDefault { + max-width: 1000px; + } `; Wrapper.displayName = 'Wrapper'; @@ -35,6 +35,7 @@ interface WrapperPageProps { className?: string; restrictWidth?: boolean | number | string; style?: Record; + noPadding?: boolean; } const WrapperPageComponent: React.FC = ({ @@ -42,6 +43,7 @@ const WrapperPageComponent: React.FC = ({ className, restrictWidth, style, + noPadding, }) => { const classes = classNames(className, { siemWrapperPage: true, @@ -58,7 +60,7 @@ const WrapperPageComponent: React.FC = ({ } return ( - + {children} diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts index 1036a74b74a03..adbfdbf6d6051 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/common.ts @@ -5,9 +5,9 @@ */ import { set } from 'lodash/fp'; +import { RequestHandlerContext } from 'src/core/server'; import { SetupPlugins } from '../../../../plugin'; import { KibanaRequest } from '../../../../../../../../src/core/server'; -import { RequestHandlerContext } from '../../../../../../../../target/types/core/server'; import { FrameworkRequest } from '../../../framework'; export const buildFrameworkRequest = async ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a50a95bfdab38..60073f8a0583c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -124,6 +124,8 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "{query} を検索しました。{sectionLenght, plural, one {# セクション} other {# セクション}}に{optionLenght, plural, one {# オプション} other {# オプション}}があります。", + "charts.advancedSettings.visualization.colorMappingText": "ビジュアライゼーション内の特定の色のマップ値です", + "charts.advancedSettings.visualization.colorMappingTitle": "カラーマッピング", "charts.colormaps.bluesText": "青", "charts.colormaps.greensText": "緑", "charts.colormaps.greenToRedText": "緑から赤", @@ -538,6 +540,10 @@ "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", + "data.advancedSettings.docTableHighlightText": "ディスカバリと保存された検索ダッシュボードの結果をハイライトします。ハイライトすることで、大きなドキュメントを扱う際にリクエストが遅くなります。", + "data.advancedSettings.docTableHighlightTitle": "結果をハイライト", + "data.advancedSettings.metaFieldsText": "_source の外にあり、ドキュメントが表示される時に融合されるフィールドです", + "data.advancedSettings.metaFieldsTitle": "メタフィールド", "data.common.kql.errors.endOfInputText": "インプットの終わり", "data.common.kql.errors.fieldNameText": "フィールド名", "data.common.kql.errors.literalText": "文字通り", @@ -847,6 +853,28 @@ "devTools.badge.readOnly.text": "読み込み専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.k7BreadcrumbsDevToolsLabel": "開発ツール", + "discover.advancedSettings.aggsTermsSizeText": "「可視化」ボタンをクリックした際に、フィールドドロップダウンやディスカバリサイドバーに可視化される用語の数を設定します。", + "discover.advancedSettings.aggsTermsSizeTitle": "用語数", + "discover.advancedSettings.context.defaultSizeText": "コンテキストビューに表示される周りのエントリーの数", + "discover.advancedSettings.context.defaultSizeTitle": "コンテキストサイズ", + "discover.advancedSettings.context.sizeStepText": "コンテキストサイズを増減させる際の最低単位です", + "discover.advancedSettings.context.sizeStepTitle": "コンテキストサイズのステップ", + "discover.advancedSettings.context.tieBreakerFieldsText": "同じタイムスタンプ値のドキュメントを区別するためのコンマ区切りのフィールドのリストです。このリストから、現在のインデックスパターンに含まれ並べ替え可能な初めのフィールドが使用されます。", + "discover.advancedSettings.context.tieBreakerFieldsTitle": "タイブレーカーフィールド", + "discover.advancedSettings.defaultColumnsText": "デフォルトでディスカバリタブに表示される列です", + "discover.advancedSettings.defaultColumnsTitle": "デフォルトの列", + "discover.advancedSettings.docTableHideTimeColumnText": "ディスカバリと、ダッシュボードのすべての保存された検索で、「時刻」列を非表示にします。", + "discover.advancedSettings.docTableHideTimeColumnTitle": "「時刻」列を非表示", + "discover.advancedSettings.fieldsPopularLimitText": "最も頻繁に使用されるフィールドのトップ N を表示します", + "discover.advancedSettings.fieldsPopularLimitTitle": "頻繁に使用されるフィールドの制限", + "discover.advancedSettings.sampleSizeText": "表に表示する行数です", + "discover.advancedSettings.sampleSizeTitle": "行数", + "discover.advancedSettings.searchOnPageLoadText": "ディスカバリの最初の読み込み時に検索を実行するかを制御します。この設定は、保存された検索の読み込み時には影響しません。", + "discover.advancedSettings.searchOnPageLoadTitle": "ページの読み込み時の検索", + "discover.advancedSettings.sortDefaultOrderText": "ディスカバリアプリのインデックスパターンに基づく時刻のデフォルトの並べ替え方向をコントロールします。", + "discover.advancedSettings.sortDefaultOrderTitle": "デフォルトの並べ替え方向", + "discover.advancedSettings.sortOrderAsc": "昇順", + "discover.advancedSettings.sortOrderDesc": "降順", "discover.docViews.json.codeEditorAriaLabel": "Elasticsearch ドキュメントの JSON ビューのみを読み込む", "discover.docViews.json.jsonTitle": "JSON", "discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", @@ -1945,12 +1973,6 @@ "inspector.requests.statisticsTabLabel": "統計", "inspector.title": "インスペクター", "inspector.view": "{viewName} を表示", - "kbn.advancedSettings.context.defaultSizeText": "コンテキストビューに表示される周りのエントリーの数", - "kbn.advancedSettings.context.defaultSizeTitle": "コンテキストサイズ", - "kbn.advancedSettings.context.sizeStepText": "コンテキストサイズを増減させる際の最低単位です", - "kbn.advancedSettings.context.sizeStepTitle": "コンテキストサイズのステップ", - "kbn.advancedSettings.context.tieBreakerFieldsText": "同じタイムスタンプ値のドキュメントを区別するためのコンマ区切りのフィールドのリストです。このリストから、現在のインデックスパターンに含まれ並べ替え可能な初めのフィールドが使用されます。", - "kbn.advancedSettings.context.tieBreakerFieldsTitle": "タイブレーカーフィールド", "kbn.advancedSettings.courier.batchSearchesText": "無効の場合、ダッシュボードパネルは個々に読み込まれ、検索リクエストはユーザーが移動するか\n クエリを更新すると停止します。有効の場合、ダッシュボードパネルはすべてのデータが読み込まれると同時に読み込まれ、\n 検索は停止しません。", "kbn.advancedSettings.courier.batchSearchesTextDeprecation": "この設定はサポートが終了し、Kibana 8.0 では削除されます。", "kbn.advancedSettings.courier.batchSearchesTitle": "同時検索のバッチ処理", @@ -1985,8 +2007,6 @@ "kbn.advancedSettings.dateNanosFormatText": "Elasticsearch の {dateNanosLink} データタイプに使用されます", "kbn.advancedSettings.dateNanosFormatTitle": "ナノ秒フォーマットでの日付", "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultColumnsText": "デフォルトでディスカバリタブに表示される列です", - "kbn.advancedSettings.defaultColumnsTitle": "デフォルトの列", "kbn.advancedSettings.defaultIndexText": "インデックスが設定されていない時にアクセスするインデックスです", "kbn.advancedSettings.defaultIndexTitle": "デフォルトのインデックス", "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "相対 URL でなければなりません。", @@ -1994,22 +2014,6 @@ "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "デフォルトのルート", "kbn.advancedSettings.disableAnimationsText": "Kibana UI の不要なアニメーションをオフにします。変更を適用するにはページを更新してください。", "kbn.advancedSettings.disableAnimationsTitle": "アニメーションを無効にする", - "kbn.advancedSettings.discover.aggsTermsSizeText": "「可視化」ボタンをクリックした際に、フィールドドロップダウンやディスカバリサイドバーに可視化される用語の数を設定します。", - "kbn.advancedSettings.discover.aggsTermsSizeTitle": "用語数", - "kbn.advancedSettings.discover.sampleSizeText": "表に表示する行数です", - "kbn.advancedSettings.discover.sampleSizeTitle": "行数", - "kbn.advancedSettings.discover.searchOnPageLoadText": "ディスカバリの最初の読み込み時に検索を実行するかを制御します。この設定は、保存された検索の読み込み時には影響しません。", - "kbn.advancedSettings.discover.searchOnPageLoadTitle": "ページの読み込み時の検索", - "kbn.advancedSettings.discover.sortDefaultOrderText": "ディスカバリアプリのインデックスパターンに基づく時刻のデフォルトの並べ替え方向をコントロールします。", - "kbn.advancedSettings.discover.sortDefaultOrderTitle": "デフォルトの並べ替え方向", - "kbn.advancedSettings.discover.sortOrderAsc": "昇順", - "kbn.advancedSettings.discover.sortOrderDesc": "降順", - "kbn.advancedSettings.docTableHideTimeColumnText": "ディスカバリと、ダッシュボードのすべての保存された検索で、「時刻」列を非表示にします。", - "kbn.advancedSettings.docTableHideTimeColumnTitle": "「時刻」列を非表示", - "kbn.advancedSettings.docTableHighlightText": "ディスカバリと保存された検索ダッシュボードの結果をハイライトします。ハイライトすることで、大きなドキュメントを扱う際にリクエストが遅くなります。", - "kbn.advancedSettings.docTableHighlightTitle": "結果をハイライト", - "kbn.advancedSettings.fieldsPopularLimitText": "最も頻繁に使用されるフィールドのトップ N を表示します", - "kbn.advancedSettings.fieldsPopularLimitTitle": "頻繁に使用されるフィールドの制限", "kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット", "kbn.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト {numeralFormatLink} です", "kbn.advancedSettings.format.bytesFormatTitle": "バイトフォーマット", @@ -2035,12 +2039,8 @@ "kbn.advancedSettings.historyLimitTitle": "履歴制限数", "kbn.advancedSettings.indexPatternPlaceholderText": "「管理 > インデックスパターン > インデックスパターンを作成」で使用される「インデックスパターン名」フィールドのプレースホルダーです。", "kbn.advancedSettings.indexPatternPlaceholderTitle": "インデックスパターンのプレースホルダー", - "kbn.advancedSettings.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です", - "kbn.advancedSettings.maxBucketsTitle": "バケットの最大数", "kbn.advancedSettings.maxCellHeightText": "表のセルが使用する高さの上限です。この切り捨てを無効にするには 0 に設定します", "kbn.advancedSettings.maxCellHeightTitle": "表のセルの高さの上限", - "kbn.advancedSettings.metaFieldsText": "_source の外にあり、ドキュメントが表示される時に融合されるフィールドです", - "kbn.advancedSettings.metaFieldsTitle": "メタフィールド", "kbn.advancedSettings.notifications.banner.markdownLinkText": "マークダウン対応", "kbn.advancedSettings.notifications.bannerLifetimeText": "バナー通知が画面に表示されるミリ秒単位での時間です。{infinityValue} に設定するとカウントダウンが無効になります。", "kbn.advancedSettings.notifications.bannerLifetimeTitle": "バナー通知時間", @@ -2059,10 +2059,6 @@ "kbn.advancedSettings.query.queryStringOptions.optionsLinkText": "オプション", "kbn.advancedSettings.query.queryStringOptionsText": "Lucene クエリ文字列パーサーの {optionsLink}「{queryLanguage}」が {luceneLanguage} に設定されている時にのみ使用されます。", "kbn.advancedSettings.query.queryStringOptionsTitle": "クエリ文字列のオプション", - "kbn.advancedSettings.savedObjects.listingLimitText": "一覧ページ用に取得するオブジェクトの数です", - "kbn.advancedSettings.savedObjects.listingLimitTitle": "オブジェクト取得制限", - "kbn.advancedSettings.savedObjects.perPageText": "読み込みダイアログで表示されるページごとのオブジェクトの数です", - "kbn.advancedSettings.savedObjects.perPageTitle": "ページごとのオブジェクト数", "kbn.advancedSettings.searchQueryLanguageKql": "KQL", "kbn.advancedSettings.searchQueryLanguageLucene": "Lucene", "kbn.advancedSettings.searchQueryLanguageText": "クエリバーで使用されるクエリ言語です。KQL は Kibana 用に特別に開発された新しい言語です。", @@ -2093,14 +2089,6 @@ "kbn.advancedSettings.timepicker.timeDefaultsText": "時間フィルターが選択されずに Kibana が起動した際に使用される時間フィルターです", "kbn.advancedSettings.timepicker.timeDefaultsTitle": "デフォルトのタイムピッカー", "kbn.advancedSettings.timepicker.today": "今日", - "kbn.advancedSettings.visualization.colorMappingText": "ビジュアライゼーション内の特定の色のマップ値です", - "kbn.advancedSettings.visualization.colorMappingTitle": "カラーマッピング", - "kbn.advancedSettings.visualization.dimmingOpacityText": "チャートの別のエレメントが選択された時に暗くなるチャート項目の透明度です。この数字が小さければ小さいほど、ハイライトされたエレメントが目立ちます。0 と 1 の間の数字で設定します。", - "kbn.advancedSettings.visualization.dimmingOpacityTitle": "減光透明度", - "kbn.advancedSettings.visualization.heatmap.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", - "kbn.advancedSettings.visualization.heatmap.maxBucketsTitle": "ヒートマップの最大バケット数", - "kbn.advancedSettings.visualization.loadingDelayText": "クエリの際にビジュアライゼーションを暗くするまでの時間です", - "kbn.advancedSettings.visualization.loadingDelayTitle": "読み込み遅延", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "用語がマップの形に合わない場合に地域マップに警告を表示するかどうかです。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "地域マップに警告を表示", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "ディメンションの説明", @@ -2465,6 +2453,10 @@ "regionMap.visParams.vectorMapLabel": "ベクトルマップ", "regionMap.visualization.unableToShowMismatchesWarningText": "次の各用語がシェイプの結合フィールドのシェイプと一致することを確認してください: {mismatches}", "regionMap.visualization.unableToShowMismatchesWarningTitle": "{mismatchesLength} {oneMismatch, plural, one { 件の結果} other { 件の結果}}をマップに表示できません", + "savedObjects.advancedSettings.listingLimitText": "一覧ページ用に取得するオブジェクトの数です", + "savedObjects.advancedSettings.listingLimitTitle": "オブジェクト取得制限", + "savedObjects.advancedSettings.perPageText": "読み込みダイアログで表示されるページごとのオブジェクトの数です", + "savedObjects.advancedSettings.perPageTitle": "ページごとのオブジェクト数", "savedObjects.confirmModal.cancelButtonLabel": "キャンセル", "savedObjects.confirmModal.overwriteButtonLabel": "上書き", "savedObjects.confirmModal.overwriteConfirmationMessage": "{title} を上書きしてよろしいですか?", @@ -3156,6 +3148,8 @@ "visTypeTimeseries.addDeleteButtons.deleteButtonDefaultTooltip": "削除", "visTypeTimeseries.addDeleteButtons.reEnableTooltip": "再度有効にする", "visTypeTimeseries.addDeleteButtons.temporarilyDisableTooltip": "一時的に無効にする", + "visTypeTimeseries.advancedSettings.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です", + "visTypeTimeseries.advancedSettings.maxBucketsTitle": "バケットの最大数", "visTypeTimeseries.aggLookup.averageLabel": "平均", "visTypeTimeseries.aggLookup.calculationLabel": "計算", "visTypeTimeseries.aggLookup.cardinalityLabel": "基数", @@ -3714,6 +3708,10 @@ "visTypeVega.visualization.renderErrorTitle": "Vega エラー", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "デフォルトのインデックスが見つかりません", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "データなしにはレンダリングできません", + "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "チャートの別のエレメントが選択された時に暗くなるチャート項目の透明度です。この数字が小さければ小さいほど、ハイライトされたエレメントが目立ちます。0 と 1 の間の数字で設定します。", + "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "減光透明度", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "1 つのデータソースが返せるバケットの最大数です。値が大きいとブラウザのレンダリング速度が下がる可能性があります。", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "ヒートマップの最大バケット数", "visTypeVislib.aggResponse.allDocsTitle": "すべてのドキュメント", "visTypeVislib.area.areaDescription": "折れ線グラフの下の数量を強調します。", "visTypeVislib.area.areaTitle": "エリア", @@ -6736,7 +6734,6 @@ "xpack.idxMgmt.indexTable.serverErrorTitle": "インデックスの読み込み中にエラーが発生", "xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel": "インデックスの検索", "xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder": "検索", - "xpack.idxMgmt.indexTable.systemIndicesSwitchLabel": "システムインデックスを含める", "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "まだテンプレートがありません", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "テンプレートを読み込み中…", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "テンプレートの読み込み中にエラーが発生", @@ -13132,7 +13129,6 @@ "xpack.siem.case.caseView.pushToServiceDisableByConfigTitle": "Kibana の構成ファイルで ServiceNow を有効にする", "xpack.siem.case.caseView.pushToServiceDisableByLicenseDescription": "外部システムでケースを開くには、ライセンスをプラチナに更新するか、30 日間の無料トライアルを開始するか、AWS、GCP、または Azure で {link} にサインアップする必要があります。", "xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle": "E lastic Platinum へのアップグレード", - "xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription": "外部システムでケースを開いて更新するには、{link} を設定する必要があります。", "xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle": "外部コネクターを構成", "xpack.siem.case.caseView.reopenCase": "ケースを再開", "xpack.siem.case.caseView.reopenedCase": "ケースを再開する", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b984c7ad94eb0..bc95b68082f76 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -124,6 +124,8 @@ "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", "advancedSettings.searchBarAriaLabel": "搜索高级设置", "advancedSettings.voiceAnnouncement.searchResultScreenReaderMessage": "您搜索了“{query}”。{sectionLenght, plural, one {# 个部分} other {# 个部分}}中有 {optionLenght, plural, one {# 个选项} other {# 个选项}}", + "charts.advancedSettings.visualization.colorMappingText": "将值映射到可视化内的指定颜色", + "charts.advancedSettings.visualization.colorMappingTitle": "颜色映射", "charts.colormaps.bluesText": "蓝色", "charts.colormaps.greensText": "绿色", "charts.colormaps.greenToRedText": "绿到红", @@ -537,6 +539,10 @@ "dashboard.panel.removePanel.replacePanel": "替换面板", "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", + "data.advancedSettings.metaFieldsText": "_source 之外存在的、在显示我们的文档时将合并进其中的字段", + "data.advancedSettings.metaFieldsTitle": "元字段", + "data.advancedSettings.docTableHighlightText": "突出显示 Discover 和已保存搜索仪表板中的结果。处理大文档时,突出显示会使请求变慢。", + "data.advancedSettings.docTableHighlightTitle": "突出显示结果", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", "data.common.kql.errors.fieldNameText": "字段名称", @@ -848,6 +854,28 @@ "devTools.badge.readOnly.text": "只读", "devTools.badge.readOnly.tooltip": "无法保存", "devTools.k7BreadcrumbsDevToolsLabel": "开发工具", + "discover.advancedSettings.aggsTermsSizeText": "确定在单击“可视化”按钮时将在发现侧边栏的字段下拉列表中可视化多少个词。", + "discover.advancedSettings.aggsTermsSizeTitle": "词数目", + "discover.advancedSettings.context.defaultSizeText": "要在上下文视图中显示的周围条目数目", + "discover.advancedSettings.context.defaultSizeTitle": "上下文大小", + "discover.advancedSettings.context.sizeStepText": "递增或递减上下文大小的步进大小", + "discover.advancedSettings.context.sizeStepTitle": "上下文大小步进", + "discover.advancedSettings.context.tieBreakerFieldsText": "要在具有相同时间戳值的文档之间用于平分决胜的字段逗号分隔列表。将使用此列表上存在且在当前索引模式下可排序的第一个字段。", + "discover.advancedSettings.context.tieBreakerFieldsTitle": "平分决胜字段", + "discover.advancedSettings.docTableHideTimeColumnText": "在 Discover 中和仪表板上的所有已保存搜索中隐藏“时间”列。", + "discover.advancedSettings.docTableHideTimeColumnTitle": "隐藏“时间”列", + "discover.advancedSettings.fieldsPopularLimitText": "要显示的排名前 N 最常见字段", + "discover.advancedSettings.fieldsPopularLimitTitle": "常见字段限制", + "discover.advancedSettings.defaultColumnsText": "“发现”选项卡中默认显示的列", + "discover.advancedSettings.defaultColumnsTitle": "默认列", + "discover.advancedSettings.sampleSizeText": "要在表中显示的行数目", + "discover.advancedSettings.sampleSizeTitle": "行数目", + "discover.advancedSettings.searchOnPageLoadText": "控制在 Discover 首次加载时是否执行搜索。加载已保存搜索时,此设置无效。", + "discover.advancedSettings.searchOnPageLoadTitle": "在页面加载时搜索", + "discover.advancedSettings.sortDefaultOrderText": "在 Discover 应用中控制基于时间的索引模式的默认排序方向。", + "discover.advancedSettings.sortDefaultOrderTitle": "默认排序方向", + "discover.advancedSettings.sortOrderAsc": "升序", + "discover.advancedSettings.sortOrderDesc": "降序", "discover.docViews.json.codeEditorAriaLabel": "Elasticsearch 文档的只读 JSON 视图", "discover.docViews.json.jsonTitle": "JSON", "discover.docViews.table.fieldNamesBeginningWithUnderscoreUnsupportedAriaLabel": "警告", @@ -1946,12 +1974,6 @@ "inspector.requests.statisticsTabLabel": "统计信息", "inspector.title": "检查器", "inspector.view": "视图:{viewName}", - "kbn.advancedSettings.context.defaultSizeText": "要在上下文视图中显示的周围条目数目", - "kbn.advancedSettings.context.defaultSizeTitle": "上下文大小", - "kbn.advancedSettings.context.sizeStepText": "递增或递减上下文大小的步进大小", - "kbn.advancedSettings.context.sizeStepTitle": "上下文大小步进", - "kbn.advancedSettings.context.tieBreakerFieldsText": "要在具有相同时间戳值的文档之间用于平分决胜的字段逗号分隔列表。将使用此列表上存在且在当前索引模式下可排序的第一个字段。", - "kbn.advancedSettings.context.tieBreakerFieldsTitle": "平分决胜字段", "kbn.advancedSettings.courier.batchSearchesText": "禁用时,仪表板面板将分别加载,用户离开时或更新查询时,\n 搜索请求将终止。启用时,仪表板面板将一起加载并加载所有数据,\n 搜索将不会终止。", "kbn.advancedSettings.courier.batchSearchesTextDeprecation": "此设置已过时,将在 Kibana 8.0 中移除。", "kbn.advancedSettings.courier.batchSearchesTitle": "批处理并发搜索", @@ -1986,8 +2008,6 @@ "kbn.advancedSettings.dateNanosFormatText": "用于 Elasticsearch 的 {dateNanosLink} 数据类型", "kbn.advancedSettings.dateNanosFormatTitle": "纳秒格式的日期", "kbn.advancedSettings.dateNanosLinkTitle": "date_nanos", - "kbn.advancedSettings.defaultColumnsText": "“发现”选项卡中默认显示的列", - "kbn.advancedSettings.defaultColumnsTitle": "默认列", "kbn.advancedSettings.defaultIndexText": "未设置索引时要访问的索引", "kbn.advancedSettings.defaultIndexTitle": "默认索引", "kbn.advancedSettings.defaultRoute.defaultRouteIsRelativeValidationMessage": "必须是相对 URL。", @@ -1995,22 +2015,6 @@ "kbn.advancedSettings.defaultRoute.defaultRouteTitle": "默认路由", "kbn.advancedSettings.disableAnimationsText": "在 Kibana UI 中关闭所有没有必要的动画。刷新页面以应用更改。", "kbn.advancedSettings.disableAnimationsTitle": "禁用动画", - "kbn.advancedSettings.discover.aggsTermsSizeText": "确定在单击“可视化”按钮时将在发现侧边栏的字段下拉列表中可视化多少个词。", - "kbn.advancedSettings.discover.aggsTermsSizeTitle": "词数目", - "kbn.advancedSettings.discover.sampleSizeText": "要在表中显示的行数目", - "kbn.advancedSettings.discover.sampleSizeTitle": "行数目", - "kbn.advancedSettings.discover.searchOnPageLoadText": "控制在 Discover 首次加载时是否执行搜索。加载已保存搜索时,此设置无效。", - "kbn.advancedSettings.discover.searchOnPageLoadTitle": "在页面加载时搜索", - "kbn.advancedSettings.discover.sortDefaultOrderText": "在 Discover 应用中控制基于时间的索引模式的默认排序方向。", - "kbn.advancedSettings.discover.sortDefaultOrderTitle": "默认排序方向", - "kbn.advancedSettings.discover.sortOrderAsc": "升序", - "kbn.advancedSettings.discover.sortOrderDesc": "降序", - "kbn.advancedSettings.docTableHideTimeColumnText": "在 Discover 中和仪表板上的所有已保存搜索中隐藏“时间”列。", - "kbn.advancedSettings.docTableHideTimeColumnTitle": "隐藏“时间”列", - "kbn.advancedSettings.docTableHighlightText": "突出显示 Discover 和已保存搜索仪表板中的结果。处理大文档时,突出显示会使请求变慢。", - "kbn.advancedSettings.docTableHighlightTitle": "突出显示结果", - "kbn.advancedSettings.fieldsPopularLimitText": "要显示的排名前 N 最常见字段", - "kbn.advancedSettings.fieldsPopularLimitTitle": "常见字段限制", "kbn.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式", "kbn.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}", "kbn.advancedSettings.format.bytesFormatTitle": "字节格式", @@ -2036,12 +2040,8 @@ "kbn.advancedSettings.historyLimitTitle": "历史记录限制", "kbn.advancedSettings.indexPatternPlaceholderText": "在“管理 > 索引模式 > 创建索引模式”中“索引模式名称”的占位符。", "kbn.advancedSettings.indexPatternPlaceholderTitle": "索引模式占位符", - "kbn.advancedSettings.maxBucketsText": "单个数据源可以返回的最大存储桶数目", - "kbn.advancedSettings.maxBucketsTitle": "最大存储桶数", "kbn.advancedSettings.maxCellHeightText": "表中单元格应占用的最大高度。设置为 0 可禁用截短", "kbn.advancedSettings.maxCellHeightTitle": "最大表单元格高度", - "kbn.advancedSettings.metaFieldsText": "_source 之外存在的、在显示我们的文档时将合并进其中的字段", - "kbn.advancedSettings.metaFieldsTitle": "元字段", "kbn.advancedSettings.notifications.banner.markdownLinkText": "Markdown 受支持", "kbn.advancedSettings.notifications.bannerLifetimeText": "在屏幕上显示横幅通知的时间(毫秒)。设置为 {infinityValue} 将禁用倒计时。", "kbn.advancedSettings.notifications.bannerLifetimeTitle": "横幅通知生存时间", @@ -2060,10 +2060,6 @@ "kbn.advancedSettings.query.queryStringOptions.optionsLinkText": "选项", "kbn.advancedSettings.query.queryStringOptionsText": "lucene 查询字符串解析器的{optionsLink}。只有将“{queryLanguage}”设置为 {luceneLanguage} 时才会使用。", "kbn.advancedSettings.query.queryStringOptionsTitle": "查询字符串选项", - "kbn.advancedSettings.savedObjects.listingLimitText": "为列表页面提取的对象数目", - "kbn.advancedSettings.savedObjects.listingLimitTitle": "对象列表限制", - "kbn.advancedSettings.savedObjects.perPageText": "在加载对话框中每页要显示的对象数目", - "kbn.advancedSettings.savedObjects.perPageTitle": "每页对象数", "kbn.advancedSettings.searchQueryLanguageKql": "KQL", "kbn.advancedSettings.searchQueryLanguageLucene": "Lucene", "kbn.advancedSettings.searchQueryLanguageText": "查询栏使用的查询语言。KQL 是专门为 Kibana 打造的新型语言。", @@ -2094,14 +2090,6 @@ "kbn.advancedSettings.timepicker.timeDefaultsText": "未使用时间筛选启动 Kibana 时要使用的时间筛选选择", "kbn.advancedSettings.timepicker.timeDefaultsTitle": "时间筛选默认值", "kbn.advancedSettings.timepicker.today": "今日", - "kbn.advancedSettings.visualization.colorMappingText": "将值映射到可视化内的指定颜色", - "kbn.advancedSettings.visualization.colorMappingTitle": "颜色映射", - "kbn.advancedSettings.visualization.dimmingOpacityText": "突出显示图表的其他元素时变暗图表项的透明度。此数字越低,突出显示的元素越突出。必须是介于 0 和 1 之间的数字。", - "kbn.advancedSettings.visualization.dimmingOpacityTitle": "变暗透明度", - "kbn.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", - "kbn.advancedSettings.visualization.heatmap.maxBucketsTitle": "热图最大存储桶数", - "kbn.advancedSettings.visualization.loadingDelayText": "在查询期间在将可视化变暗之前要等待的时间", - "kbn.advancedSettings.visualization.loadingDelayTitle": "加载延迟", "kbn.advancedSettings.visualization.showRegionMapWarningsText": "词无法联接到地图上的形状时,区域地图是否显示警告。", "kbn.advancedSettings.visualization.showRegionMapWarningsTitle": "显示区域地图警告", "kbn.advancedSettings.visualization.tileMap.maxPrecision.cellDimensionsLinkText": "单元格维度的解释", @@ -2466,6 +2454,10 @@ "regionMap.visParams.vectorMapLabel": "矢量地图", "regionMap.visualization.unableToShowMismatchesWarningText": "确保每个字词与该形状的联接字段匹配:{mismatches}", "regionMap.visualization.unableToShowMismatchesWarningTitle": "无法在地图上显示 {mismatchesLength} {oneMismatch, plural, one { 个结果} other { 个结果}}", + "savedObjects.advancedSettings.listingLimitText": "为列表页面提取的对象数目", + "savedObjects.advancedSettings.listingLimitTitle": "对象列表限制", + "savedObjects.advancedSettings.perPageText": "在加载对话框中每页要显示的对象数目", + "savedObjects.advancedSettings.perPageTitle": "每页对象数", "savedObjects.confirmModal.cancelButtonLabel": "取消", "savedObjects.confirmModal.overwriteButtonLabel": "覆盖", "savedObjects.confirmModal.overwriteConfirmationMessage": "确定要覆盖“{title}”?", @@ -3157,6 +3149,8 @@ "visTypeTimeseries.addDeleteButtons.deleteButtonDefaultTooltip": "删除", "visTypeTimeseries.addDeleteButtons.reEnableTooltip": "重新启用", "visTypeTimeseries.addDeleteButtons.temporarilyDisableTooltip": "暂时禁用", + "visTypeTimeseries.advancedSettings.maxBucketsText": "单个数据源可以返回的最大存储桶数目", + "visTypeTimeseries.advancedSettings.maxBucketsTitle": "最大存储桶数", "visTypeTimeseries.aggLookup.averageLabel": "平均值", "visTypeTimeseries.aggLookup.calculationLabel": "计算", "visTypeTimeseries.aggLookup.cardinalityLabel": "基数", @@ -3715,6 +3709,10 @@ "visTypeVega.visualization.renderErrorTitle": "Vega 错误", "visTypeVega.visualization.unableToFindDefaultIndexErrorMessage": "找不到默认索引", "visTypeVega.visualization.unableToRenderWithoutDataWarningMessage": "没有数据时无法渲染", + "visTypeVislib.advancedSettings.visualization.dimmingOpacityText": "突出显示图表的其他元素时变暗图表项的透明度。此数字越低,突出显示的元素越突出。必须是介于 0 和 1 之间的数字。", + "visTypeVislib.advancedSettings.visualization.dimmingOpacityTitle": "变暗透明度", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsText": "单个数据源可以返回的最大存储桶数目。较高的数目可能对浏览器呈现性能有负面影响", + "visTypeVislib.advancedSettings.visualization.heatmap.maxBucketsTitle": "热图最大存储桶数", "visTypeVislib.aggResponse.allDocsTitle": "所有文档", "visTypeVislib.area.areaDescription": "突出折线图下方的数量", "visTypeVislib.area.areaTitle": "面积图", @@ -6741,7 +6739,6 @@ "xpack.idxMgmt.indexTable.serverErrorTitle": "加载索引时出错", "xpack.idxMgmt.indexTable.systemIndicesSearchIndicesAriaLabel": "搜索索引", "xpack.idxMgmt.indexTable.systemIndicesSearchInputPlaceholder": "搜索", - "xpack.idxMgmt.indexTable.systemIndicesSwitchLabel": "包括系统索引", "xpack.idxMgmt.indexTemplatesList.emptyPrompt.noIndexTemplatesTitle": "您尚未有任何模板", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesDescription": "正在加载模板……", "xpack.idxMgmt.indexTemplatesList.loadingIndexTemplatesErrorMessage": "加载模板时出错", @@ -13139,7 +13136,6 @@ "xpack.siem.case.caseView.pushToServiceDisableByConfigTitle": "在 Kibana 配置文件中启用 ServiceNow", "xpack.siem.case.caseView.pushToServiceDisableByLicenseDescription": "要在外部系统中打开案例,必须将许可证更新到白金级,开始为期 30 天的免费试用,或在 AWS、GCP 或 Azure 上快速部署 {link}。", "xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle": "升级到 Elastic 白金级", - "xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigDescription": "要在外部系统上打开和更新案例,必须配置 {link}。", "xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle": "配置外部连接器", "xpack.siem.case.caseView.reopenCase": "重新打开案例", "xpack.siem.case.caseView.reopenedCase": "重新打开的案例", diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx index d38f203739cea..76b989671e2c1 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state.tsx @@ -6,11 +6,11 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { IHttpFetchError } from 'src/core/public'; import { EmptyStateError } from './empty_state_error'; import { EmptyStateLoading } from './empty_state_loading'; import { DataOrIndexMissing } from './data_or_index_missing'; import { DynamicSettings, StatesIndexStatus } from '../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; interface EmptyStateProps { children: JSX.Element[] | JSX.Element; diff --git a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx index aa4040e319e0f..f7b77df8497f9 100644 --- a/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/empty_state/empty_state_error.tsx @@ -7,7 +7,7 @@ import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { Fragment } from 'react'; -import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from 'src/core/public'; interface EmptyStateErrorProps { errors: IHttpFetchError[]; diff --git a/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts index 524044f873687..fb4e70977b3a8 100644 --- a/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts +++ b/x-pack/plugins/uptime/public/state/actions/monitor_duration.ts @@ -5,9 +5,9 @@ */ import { createAction } from 'redux-actions'; +import { IHttpFetchError } from 'src/core/public'; import { QueryParams } from './types'; import { MonitorDurationResult } from '../../../common/types'; -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; type MonitorQueryParams = QueryParams & { monitorId: string }; diff --git a/x-pack/plugins/uptime/public/state/actions/types.ts b/x-pack/plugins/uptime/public/state/actions/types.ts index dee2df77707d2..d752c8b3781fc 100644 --- a/x-pack/plugins/uptime/public/state/actions/types.ts +++ b/x-pack/plugins/uptime/public/state/actions/types.ts @@ -5,7 +5,7 @@ */ import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from 'src/core/public'; export interface AsyncAction { get: (payload: Payload) => Action; diff --git a/x-pack/plugins/uptime/public/state/actions/utils.ts b/x-pack/plugins/uptime/public/state/actions/utils.ts index 8ce4cf011406b..5fb2b37298df6 100644 --- a/x-pack/plugins/uptime/public/state/actions/utils.ts +++ b/x-pack/plugins/uptime/public/state/actions/utils.ts @@ -5,8 +5,8 @@ */ import { createAction } from 'redux-actions'; +import { IHttpFetchError } from 'src/core/public'; import { AsyncAction, AsyncAction1 } from './types'; -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; export function createAsyncAction( actionStr: string diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index acd9bec5a74bc..f2efd2ecb875c 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -6,7 +6,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; -import { HttpFetchQuery, HttpSetup } from '../../../../../../target/types/core/public'; +import { HttpFetchQuery, HttpSetup } from 'src/core/public'; class ApiService { private static instance: ApiService; diff --git a/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts index 0aa85609fe4f0..6535001cfc5ef 100644 --- a/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts +++ b/x-pack/plugins/uptime/public/state/effects/fetch_effect.ts @@ -6,7 +6,7 @@ import { call, put } from 'redux-saga/effects'; import { Action } from 'redux-actions'; -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from 'src/core/public'; /** * Factory function for a fetch effect. It expects three action creators, diff --git a/x-pack/plugins/uptime/public/state/reducers/types.ts b/x-pack/plugins/uptime/public/state/reducers/types.ts index c81ee6875f305..885296c0928ac 100644 --- a/x-pack/plugins/uptime/public/state/reducers/types.ts +++ b/x-pack/plugins/uptime/public/state/reducers/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IHttpFetchError } from '../../../../../../target/types/core/public/http'; +import { IHttpFetchError } from 'src/core/public'; export interface AsyncInitialState { data: ReduceStateType | null; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 8ea554c42d6e0..b00150467de00 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -9,7 +9,8 @@ const alwaysImportedTests = [ require.resolve('../test/functional_endpoint_ingest_failure/config.ts'), require.resolve('../test/functional_endpoint/config.ts'), require.resolve('../test/functional_with_es_ssl/config.ts'), - require.resolve('../test/functional/config_security_basic.js'), + require.resolve('../test/functional/config_security_basic.ts'), + require.resolve('../test/functional/config_security_trial.ts'), require.resolve('../test/plugin_functional/config.ts'), ]; const onlyNotInCoverageTests = [ diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index ac50a20b02e3b..5d523262d7f10 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -184,6 +184,7 @@ export default function({ getService }) { const { body } = await list().expect(200); const expectedKeys = [ 'health', + 'hidden', 'status', 'name', 'uuid', @@ -214,6 +215,7 @@ export default function({ getService }) { const { body } = await reload().expect(200); const expectedKeys = [ 'health', + 'hidden', 'status', 'name', 'uuid', diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts index f9c248ec3d56f..e711560e11097 100644 --- a/x-pack/test/case_api_integration/basic/config.ts +++ b/x-pack/test/case_api_integration/basic/config.ts @@ -9,6 +9,6 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('basic', { disabledPlugins: [], - license: 'basic', + license: 'trial', ssl: true, }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index 848b980dee769..2c1c4369e3ccd 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -6,6 +6,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../alerting_api_integration/common/lib'; import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../plugins/case/common/constants'; import { postCaseReq, defaultUser, postCommentReq } from '../../../common/lib/mock'; @@ -15,6 +16,7 @@ import { deleteComments, deleteConfiguration, getConfiguration, + getConnector, } from '../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -23,19 +25,31 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); describe('push_case', () => { + const actionsRemover = new ActionsRemover(supertest); + afterEach(async () => { await deleteCases(es); await deleteComments(es); await deleteConfiguration(es); await deleteCasesUserActions(es); + await actionsRemover.removeAll(); }); it('should push a case', async () => { + const { body: connector } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'true') + .send(getConnector()) + .expect(200); + + actionsRemover.add('default', connector.id, 'action'); + const { body: configure } = await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(getConfiguration()) + .send(getConfiguration(connector.id)) .expect(200); + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -58,11 +72,20 @@ export default ({ getService }: FtrProviderContext): void => { }); it('pushes a comment appropriately', async () => { + const { body: connector } = await supertest + .post('/api/action') + .set('kbn-xsrf', 'true') + .send(getConnector()) + .expect(200); + + actionsRemover.add('default', connector.id, 'action'); + const { body: configure } = await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(getConfiguration()) + .send(getConfiguration(connector.id)) .expect(200); + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -99,6 +122,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); expect(body.comments[0].pushed_by).to.eql(defaultUser); }); + it('unhappy path - 404s when case does not exist', async () => { await supertest .post(`${CASES_URL}/fake-id/_push`) diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 862705ab9610b..8df4ff66c2a2a 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -24,6 +24,7 @@ const enabledActionTypes = [ '.pagerduty', '.server-log', '.servicenow', + '.jira', '.slack', '.webhook', 'test.authorization', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 4b1dc6ffa5891..5861db2eb8e5b 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -23,6 +23,37 @@ export const getConfigurationOutput = (update = false): Partial ({ + name: 'ServiceNow Connector', + actionTypeId: '.servicenow', + secrets: { + username: 'admin', + password: 'password', + }, + config: { + apiUrl: 'http://some.non.existent.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); + export const removeServerGeneratedPropertiesFromConfigure = ( config: Partial ): Partial => { diff --git a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts index 46760a84e8a37..2574617eb5aa3 100644 --- a/x-pack/test/functional/apps/dashboard/_async_dashboard.ts +++ b/x-pack/test/functional/apps/dashboard/_async_dashboard.ts @@ -26,8 +26,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); - // FLAKY: https://github.com/elastic/kibana/issues/65949 - describe.skip('sample data dashboard', function describeIndexTests() { + describe('sample data dashboard', function describeIndexTests() { before(async () => { await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { diff --git a/x-pack/test/functional/apps/security/security.ts b/x-pack/test/functional/apps/security/security.ts index 3447f77aa7fd6..5bf7bc85eca8b 100644 --- a/x-pack/test/functional/apps/security/security.ts +++ b/x-pack/test/functional/apps/security/security.ts @@ -67,7 +67,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { }); it('logging out of a non-default space redirects to the login page at the server root', async () => { - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); diff --git a/x-pack/test/functional/apps/security/trial_license/index.ts b/x-pack/test/functional/apps/security/trial_license/index.ts new file mode 100644 index 0000000000000..0109d01ed4cff --- /dev/null +++ b/x-pack/test/functional/apps/security/trial_license/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('security app - trial license', function() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./login_selector')); + }); +} diff --git a/x-pack/test/functional/apps/security/trial_license/login_selector.ts b/x-pack/test/functional/apps/security/trial_license/login_selector.ts new file mode 100644 index 0000000000000..14f9ce99556db --- /dev/null +++ b/x-pack/test/functional/apps/security/trial_license/login_selector.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { parse } from 'url'; +import { USERS_PATH } from '../../../../../plugins/security/public/management/management_urls'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const browser = getService('browser'); + const PageObjects = getPageObjects(['security', 'common']); + + describe('Login Selector', function() { + this.tags('includeFirefox'); + + before(async () => { + await getService('esSupertest') + .post('/_security/role_mapping/saml1') + .send({ roles: ['superuser'], enabled: true, rules: { field: { 'realm.name': 'saml1' } } }) + .expect(200); + + await esArchiver.load('empty_kibana'); + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + beforeEach(async () => { + await browser.get(`${PageObjects.common.getHostPort()}/login`); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + }); + + afterEach(async () => { + await PageObjects.security.forceLogout(); + }); + + it('can login with Login Form preserving original URL', async () => { + await PageObjects.common.navigateToActualUrl('kibana', USERS_PATH, { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.common.waitUntilUrlIncludes('next='); + + await PageObjects.security.loginSelector.login('basic', 'basic1'); + + // We need to make sure that both path and hash are respected. + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/kibana'); + expect(currentURL.hash).to.eql(`#${USERS_PATH}`); + }); + + it('can login with SSO preserving original URL', async () => { + await PageObjects.common.navigateToActualUrl('kibana', USERS_PATH, { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.common.waitUntilUrlIncludes('next='); + + await PageObjects.security.loginSelector.login('saml', 'saml1'); + + // We need to make sure that both path and hash are respected. + const currentURL = parse(await browser.getCurrentUrl()); + expect(currentURL.pathname).to.eql('/app/kibana'); + expect(currentURL.hash).to.eql(`#${USERS_PATH}`); + }); + + it('should show toast with error if SSO fails', async () => { + await PageObjects.security.loginSelector.selectLoginMethod('saml', 'unknown_saml'); + + const toastTitle = await PageObjects.common.closeToast(); + expect(toastTitle).to.eql('Could not perform login.'); + + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + }); + + it('can go to Login Form and return back to Selector', async () => { + await PageObjects.security.loginSelector.selectLoginMethod('basic', 'basic1'); + await PageObjects.security.loginSelector.verifyLoginFormIsVisible(); + + await testSubjects.click('loginBackToSelector'); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + + await PageObjects.security.loginSelector.login('saml', 'saml1'); + }); + + it('can show Login Help from both Login Selector and Login Form', async () => { + // Show Login Help from Login Selector. + await testSubjects.click('loginHelpLink'); + await PageObjects.security.loginSelector.verifyLoginHelpIsVisible('Some-login-help.'); + + // Go back to Login Selector. + await testSubjects.click('loginBackToLoginLink'); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + + // Go to Login Form and show Login Help there. + await PageObjects.security.loginSelector.selectLoginMethod('basic', 'basic1'); + await PageObjects.security.loginSelector.verifyLoginFormIsVisible(); + await testSubjects.click('loginHelpLink'); + await PageObjects.security.loginSelector.verifyLoginHelpIsVisible('Some-login-help.'); + + // Go back to Login Form. + await testSubjects.click('loginBackToLoginLink'); + await PageObjects.security.loginSelector.verifyLoginFormIsVisible(); + + // Go back to Login Selector and show Login Help there again. + await testSubjects.click('loginBackToSelector'); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + await testSubjects.click('loginHelpLink'); + await PageObjects.security.loginSelector.verifyLoginHelpIsVisible('Some-login-help.'); + + // Go back to Login Selector. + await testSubjects.click('loginBackToLoginLink'); + await PageObjects.security.loginSelector.verifyLoginSelectorIsVisible(); + }); + }); +} diff --git a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts index 0b5718e92e38e..0f9fa2aed164a 100644 --- a/x-pack/test/functional/apps/spaces/copy_saved_objects.ts +++ b/x-pack/test/functional/apps/spaces/copy_saved_objects.ts @@ -32,7 +32,7 @@ export default function spaceSelectorFunctonalTests({ disabledFeatures: [], }); - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index d45b8a1ea4cdb..77d2db6c00c91 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -27,7 +27,7 @@ export default function enterSpaceFunctonalTests({ it('falls back to the default home page when the configured default route is malformed', async () => { const spaceId = 'default'; - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); @@ -39,7 +39,7 @@ export default function enterSpaceFunctonalTests({ it('allows user to navigate to different spaces, respecting the configured default route', async () => { const spaceId = 'another-space'; - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index c3fb93f4e4572..b77b656f7af56 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -36,7 +36,7 @@ export default function spaceSelectorFunctonalTests({ it('allows user to navigate to different spaces', async () => { const spaceId = 'another-space'; - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); @@ -67,7 +67,7 @@ export default function spaceSelectorFunctonalTests({ before(async () => { await esArchiver.load('spaces/selector'); - await PageObjects.security.login(null, null, { + await PageObjects.security.login(undefined, undefined, { expectSpaceSelector: true, }); await PageObjects.spaceSelector.clickSpaceCard('default'); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index aac8e8d8ef5ad..d94132efb1644 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -409,10 +409,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.security.login( 'no_visualize_privileges_user', 'no_visualize_privileges_user-password', - { - expectSpaceSelector: false, - shouldLoginIfPrompted: false, - } + { expectSpaceSelector: false } ); }); diff --git a/x-pack/test/functional/config_security_basic.js b/x-pack/test/functional/config_security_basic.ts similarity index 93% rename from x-pack/test/functional/config_security_basic.js rename to x-pack/test/functional/config_security_basic.ts index 2bb59796b5517..185c41c48e115 100644 --- a/x-pack/test/functional/config_security_basic.js +++ b/x-pack/test/functional/config_security_basic.ts @@ -8,12 +8,13 @@ import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; import { pageObjects } from './page_objects'; // the default export of config files must be a config provider // that returns an object with the projects config values -export default async function({ readConfigFile }) { +export default async function({ readConfigFile }: FtrConfigProviderContext) { const kibanaCommonConfig = await readConfigFile( require.resolve('../../../test/common/config.js') ); diff --git a/x-pack/test/functional/config_security_trial.ts b/x-pack/test/functional/config_security_trial.ts new file mode 100644 index 0000000000000..4a3e7858b7dd8 --- /dev/null +++ b/x-pack/test/functional/config_security_trial.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable import/no-default-export */ + +import { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; +import { pageObjects } from './page_objects'; + +// the default export of config files must be a config provider +// that returns an object with the projects config values +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const kibanaCommonConfig = await readConfigFile( + require.resolve('../../../test/common/config.js') + ); + const kibanaFunctionalConfig = await readConfigFile( + require.resolve('../../../test/functional/config.js') + ); + + const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port'); + const idpPath = resolve(__dirname, '../saml_api_integration/fixtures/saml_provider/metadata.xml'); + const samlIdPPlugin = resolve(__dirname, '../saml_api_integration/fixtures/saml_provider'); + + return { + testFiles: [resolve(__dirname, './apps/security/trial_license')], + + services, + pageObjects, + + servers: kibanaFunctionalConfig.get('servers'), + + esTestCluster: { + license: 'trial', + from: 'snapshot', + serverArgs: [ + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.realms.saml.saml1.order=0', + `xpack.security.authc.realms.saml.saml1.idp.metadata.path=${idpPath}`, + 'xpack.security.authc.realms.saml.saml1.idp.entity_id=http://www.elastic.co/saml1', + `xpack.security.authc.realms.saml.saml1.sp.entity_id=http://localhost:${kibanaPort}`, + `xpack.security.authc.realms.saml.saml1.sp.logout=http://localhost:${kibanaPort}/logout`, + `xpack.security.authc.realms.saml.saml1.sp.acs=http://localhost:${kibanaPort}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.saml1.attributes.principal=urn:oid:0.0.7', + ], + }, + + kbnTestServer: { + ...kibanaCommonConfig.get('kbnTestServer'), + serverArgs: [ + ...kibanaCommonConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${samlIdPPlugin}`, + '--server.uuid=5b2de169-2785-441b-ae8c-186a1936b17d', + '--xpack.security.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', + `--xpack.security.loginHelp="Some-login-help."`, + '--xpack.security.authc.providers.basic.basic1.order=0', + '--xpack.security.authc.providers.saml.saml1.order=1', + '--xpack.security.authc.providers.saml.saml1.realm=saml1', + '--xpack.security.authc.providers.saml.saml1.description="Log-in-with-SAML"', + '--xpack.security.authc.providers.saml.saml1.icon=logoKibana', + '--xpack.security.authc.providers.saml.unknown_saml.order=2', + '--xpack.security.authc.providers.saml.unknown_saml.realm=unknown_realm', + '--xpack.security.authc.providers.saml.unknown_saml.description="Do-not-log-in-with-THIS-SAML"', + '--xpack.security.authc.providers.saml.unknown_saml.icon=logoAWS', + ], + }, + uiSettings: { + defaults: { + 'accessibility:disableAnimations': true, + 'dateFormat:tz': 'UTC', + }, + }, + apps: kibanaFunctionalConfig.get('apps'), + esArchiver: { directory: resolve(__dirname, 'es_archives') }, + screenshots: { directory: resolve(__dirname, 'screenshots') }, + + junit: { + reportName: 'Chrome X-Pack UI Functional Tests', + }, + }; +} diff --git a/x-pack/test/functional/page_objects/monitoring_page.js b/x-pack/test/functional/page_objects/monitoring_page.js index 323c01e234880..ece0c0a6c7854 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.js +++ b/x-pack/test/functional/page_objects/monitoring_page.js @@ -5,7 +5,7 @@ */ export function MonitoringPageProvider({ getPageObjects, getService }) { - const PageObjects = getPageObjects(['common', 'header', 'security', 'shield', 'spaceSelector']); + const PageObjects = getPageObjects(['common', 'header', 'security', 'login', 'spaceSelector']); const testSubjects = getService('testSubjects'); const security = getService('security'); @@ -20,7 +20,7 @@ export function MonitoringPageProvider({ getPageObjects, getService }) { if (!useSuperUser) { await PageObjects.security.forceLogout(); - await PageObjects.shield.login('basic_monitoring_user', 'monitoring_user_password'); + await PageObjects.login.login('basic_monitoring_user', 'monitoring_user_password'); } await PageObjects.common.navigateToApp('monitoring'); } diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.ts similarity index 61% rename from x-pack/test/functional/page_objects/security_page.js rename to x-pack/test/functional/page_objects/security_page.ts index ae26a831d4172..ac549fff12e65 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { map as mapAsync } from 'bluebird'; +import { FtrProviderContext } from '../ftr_provider_context'; +import { Role } from '../../../plugins/security/common/model'; -export function SecurityPageProvider({ getService, getPageObjects }) { +export function SecurityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const config = getService('config'); const retry = getService('retry'); @@ -17,53 +18,116 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const userMenu = getService('userMenu'); const PageObjects = getPageObjects(['common', 'header', 'settings', 'home', 'error']); - class LoginPage { - async login(username, password, options = {}) { - const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':'); + interface LoginOptions { + expectSpaceSelector?: boolean; + expectSuccess?: boolean; + expectForbidden?: boolean; + } + + type LoginExpectedResult = 'spaceSelector' | 'error' | 'chrome'; + + async function waitForLoginPage() { + log.debug('Waiting for Login Page to appear.'); + await retry.waitForWithTimeout('login page', config.get('timeouts.waitFor') * 5, async () => { + // As a part of the cleanup flow tests usually try to log users out, but there are cases when + // browser/Kibana would like users to confirm that they want to navigate away from the current + // page and lose the state (e.g. unsaved changes) via native alert dialog. + const alert = await browser.getAlert(); + if (alert && alert.accept) { + await alert.accept(); + } + return await find.existsByDisplayedByCssSelector('.login-form'); + }); + } - username = username || superUsername; - password = password || superPassword; + async function waitForLoginForm() { + log.debug('Waiting for Login Form to appear.'); + await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => { + return await testSubjects.exists('loginForm'); + }); + } + + async function waitForLoginSelector() { + log.debug('Waiting for Login Selector to appear.'); + await retry.waitForWithTimeout( + 'login selector', + config.get('timeouts.waitFor') * 5, + async () => { + return await testSubjects.exists('loginSelector'); + } + ); + } - const expectSpaceSelector = options.expectSpaceSelector || false; - const expectSuccess = options.expectSuccess; - const expectForbidden = options.expectForbidden || false; + async function waitForLoginHelp(helpText: string) { + log.debug(`Waiting for Login Help to appear with text: ${helpText}.`); + await retry.waitForWithTimeout('login help', config.get('timeouts.waitFor') * 5, async () => { + return (await testSubjects.getVisibleText('loginHelp')) === helpText; + }); + } + + async function waitForLoginResult(expectedResult?: LoginExpectedResult) { + log.debug(`Waiting for login result, expected: ${expectedResult}.`); + + // wait for either space selector, kibanaChrome or loginErrorMessage + if (expectedResult === 'spaceSelector') { + await retry.try(() => testSubjects.find('kibanaSpaceSelector')); + log.debug( + `Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}` + ); + return; + } + + if (expectedResult === 'error') { const rawDataTabLocator = 'a[id=rawdata-tab]'; + if (await find.existsByCssSelector(rawDataTabLocator)) { + // Firefox has 3 tabs and requires navigation to see Raw output + await find.clickByCssSelector(rawDataTabLocator); + } + await retry.try(async () => { + if (await find.existsByCssSelector(rawDataTabLocator)) { + await find.clickByCssSelector(rawDataTabLocator); + } + await PageObjects.error.expectForbidden(); + }); + log.debug( + `Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}` + ); + return; + } + + if (expectedResult === 'chrome') { + await find.byCssSelector( + '[data-test-subj="kibanaChrome"] .app-wrapper:not(.hidden-chrome)', + 20000 + ); + log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); + } + } + const loginPage = Object.freeze({ + async login(username?: string, password?: string, options: LoginOptions = {}) { await PageObjects.common.navigateToApp('login'); // ensure welcome screen won't be shown. This is relevant for environments which don't allow // to use the yml setting, e.g. cloud await browser.setLocalStorageItem('home:welcome:show', 'false'); + await waitForLoginForm(); - await testSubjects.setValue('loginUsername', username); - await testSubjects.setValue('loginPassword', password); + const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':'); + await testSubjects.setValue('loginUsername', username || superUsername); + await testSubjects.setValue('loginPassword', password || superPassword); await testSubjects.click('loginSubmit'); - // wait for either space selector, kibanaChrome or loginErrorMessage - if (expectSpaceSelector) { - await retry.try(() => testSubjects.find('kibanaSpaceSelector')); - log.debug( - `Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}` - ); - } else if (expectForbidden) { - if (await find.existsByCssSelector(rawDataTabLocator)) { - // Firefox has 3 tabs and requires navigation to see Raw output - await find.clickByCssSelector(rawDataTabLocator); - } - await retry.try(async () => { - if (await find.existsByCssSelector(rawDataTabLocator)) { - await find.clickByCssSelector(rawDataTabLocator); - } - await PageObjects.error.expectForbidden(); - }); - log.debug( - `Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}` - ); - } else if (expectSuccess) { - await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); - log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); - } - } + await waitForLoginResult( + options.expectSpaceSelector + ? 'spaceSelector' + : options.expectForbidden + ? 'error' + : options.expectSuccess + ? 'chrome' + : undefined + ); + }, async getErrorMessage() { return await retry.try(async () => { @@ -76,13 +140,53 @@ export function SecurityPageProvider({ getService, getPageObjects }) { return errorMessageText; }); - } - } + }, + }); + + const loginSelector = Object.freeze({ + async login(providerType: string, providerName: string, options?: Record) { + log.debug(`Starting login flow for ${providerType}/${providerName}`); + + await this.verifyLoginSelectorIsVisible(); + await this.selectLoginMethod(providerType, providerName); + + if (providerType === 'basic' || providerType === 'token') { + await waitForLoginForm(); + + const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':'); + await testSubjects.setValue('loginUsername', options?.username ?? superUsername); + await testSubjects.setValue('loginPassword', options?.password ?? superPassword); + await testSubjects.click('loginSubmit'); + } + + await waitForLoginResult('chrome'); + + log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); + }, + + async selectLoginMethod(providerType: string, providerName: string) { + // Ensure welcome screen won't be shown. This is relevant for environments which don't allow + // to use the yml setting, e.g. cloud. + await browser.setLocalStorageItem('home:welcome:show', 'false'); + await testSubjects.click(`loginCard-${providerType}/${providerName}`); + }, + + async verifyLoginFormIsVisible() { + await waitForLoginForm(); + }, + + async verifyLoginSelectorIsVisible() { + await waitForLoginSelector(); + }, + + async verifyLoginHelpIsVisible(helpText: string) { + await waitForLoginHelp(helpText); + }, + }); class SecurityPage { - constructor() { - this.loginPage = new LoginPage(); - } + public loginPage = loginPage; + public loginSelector = loginSelector; async initTests() { log.debug('SecurityPage:initTests'); @@ -91,7 +195,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await browser.setWindowSize(1600, 1000); } - async login(username, password, options = {}) { + async login(username?: string, password?: string, options: LoginOptions = {}) { await this.loginPage.login(username, password, options); if (options.expectSpaceSelector || options.expectForbidden) { @@ -110,7 +214,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } await userMenu.clickLogoutButton(); - await this.waitForLoginForm(); + await waitForLoginPage(); } async forceLogout() { @@ -124,17 +228,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const url = PageObjects.common.getHostPort() + '/logout'; await browser.get(url); log.debug('Waiting on the login form to appear'); - await this.waitForLoginForm(); - } - - async waitForLoginForm() { - await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => { - const alert = await browser.getAlert(); - if (alert && alert.accept) { - await alert.accept(); - } - return await find.existsByDisplayedByCssSelector('.login-form'); - }); + await waitForLoginPage(); } async clickRolesSection() { @@ -153,14 +247,10 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await retry.try(() => testSubjects.click('createRoleButton')); } - async clickCloneRole(roleName) { + async clickCloneRole(roleName: string) { await retry.try(() => testSubjects.click(`clone-role-action-${roleName}`)); } - async getCreateIndexPatternInputFieldExists() { - return await testSubjects.exists('createIndexPatternNameInput'); - } - async clickCancelEditUser() { await testSubjects.click('userFormCancelButton'); } @@ -181,7 +271,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await PageObjects.header.waitUntilLoadingHasFinished(); } - async addIndexToRole(index) { + async addIndexToRole(index: string) { log.debug(`Adding index ${index} to role`); const indexInput = await retry.try(() => find.byCssSelector('[data-test-subj="indicesInput0"] input') @@ -190,7 +280,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await indexInput.type('\n'); } - async addPrivilegeToRole(privilege) { + async addPrivilegeToRole(privilege: string) { log.debug(`Adding privilege ${privilege} to role`); const privilegeInput = await retry.try(() => find.byCssSelector('[data-test-subj="privilegesInput0"] input') @@ -208,7 +298,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { // await options.click(); } - async assignRoleToUser(role) { + async assignRoleToUser(role: string) { await this.selectRole(role); } @@ -227,8 +317,8 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } async getElasticsearchUsers() { - const users = await testSubjects.findAll('userRow'); - return mapAsync(users, async user => { + const users = []; + for (const user of await testSubjects.findAll('userRow')) { const fullnameElement = await user.findByTestSubject('userRowFullName'); const usernameElement = await user.findByTestSubject('userRowUserName'); const emailElement = await user.findByTestSubject('userRowEmail'); @@ -237,20 +327,22 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const isUserReserved = (await user.findAllByTestSubject('userReserved', 1)).length > 0; const isUserDeprecated = (await user.findAllByTestSubject('userDeprecated', 1)).length > 0; - return { + users.push({ username: await usernameElement.getVisibleText(), fullname: await fullnameElement.getVisibleText(), email: await emailElement.getVisibleText(), roles: (await rolesElement.getVisibleText()).split('\n').map(role => role.trim()), reserved: isUserReserved, deprecated: isUserDeprecated, - }; - }); + }); + } + + return users; } async getElasticsearchRoles() { - const users = await testSubjects.findAll('roleRow'); - return mapAsync(users, async role => { + const roles = []; + for (const role of await testSubjects.findAll('roleRow')) { const [rolename, reserved, deprecated] = await Promise.all([ role.findByTestSubject('roleRowName').then(el => el.getVisibleText()), // findAll is substantially faster than `find.descendantExistsByCssSelector for negative cases @@ -259,12 +351,10 @@ export function SecurityPageProvider({ getService, getPageObjects }) { role.findAllByTestSubject('roleDeprecated', 1).then(el => el.length > 0), ]); - return { - rolename, - reserved, - deprecated, - }; - }); + roles.push({ rolename, reserved, deprecated }); + } + + return roles; } async clickNewUser() { @@ -275,7 +365,15 @@ export function SecurityPageProvider({ getService, getPageObjects }) { return await testSubjects.click('createRoleButton'); } - async addUser(userObj) { + async addUser(userObj: { + username: string; + password: string; + confirmPassword: string; + email: string; + fullname: string; + roles: string[]; + save?: boolean; + }) { const self = this; await this.clickNewUser(); log.debug('username = ' + userObj.username); @@ -302,35 +400,36 @@ export function SecurityPageProvider({ getService, getPageObjects }) { } } - addRole(roleName, userObj) { + addRole(roleName: string, roleObj: Role) { const self = this; return ( this.clickNewRole() .then(function() { // We have to use non-test-subject selectors because this markup is generated by ui-select. - log.debug('userObj.indices[0].names = ' + userObj.elasticsearch.indices[0].names); + log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); return testSubjects.append('roleFormNameInput', roleName); }) .then(function() { return find.setValue( '[data-test-subj="indicesInput0"] input', - userObj.elasticsearch.indices[0].names + '\n' + roleObj.elasticsearch.indices[0].names + '\n' ); }) .then(function() { return testSubjects.click('restrictDocumentsQuery0'); }) .then(function() { - if (userObj.elasticsearch.indices[0].query) { - return testSubjects.setValue('queryInput0', userObj.elasticsearch.indices[0].query); + if (roleObj.elasticsearch.indices[0].query) { + return testSubjects.setValue('queryInput0', roleObj.elasticsearch.indices[0].query); } }) - //KibanaPriv - .then(function() { - function addKibanaPriv(priv) { - return priv.reduce(async function(promise, privName) { + // KibanaPrivilege + .then(async () => { + const globalPrivileges = (roleObj.kibana as any).global; + if (globalPrivileges) { + for (const privilegeName of globalPrivileges) { const button = await testSubjects.find('addSpacePrivilegeButton'); await button.click(); @@ -343,33 +442,30 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const basePrivilegeSelector = await testSubjects.find('basePrivilegeComboBox'); await basePrivilegeSelector.click(); - const privilegeOption = await find.byCssSelector(`#basePrivilege_${privName}`); + const privilegeOption = await find.byCssSelector(`#basePrivilege_${privilegeName}`); await privilegeOption.click(); const createPrivilegeButton = await testSubjects.find('createSpacePrivilegeButton'); await createPrivilegeButton.click(); - - return promise; - }, Promise.resolve()); + } } - return userObj.kibana.global ? addKibanaPriv(userObj.kibana.global) : Promise.resolve(); }) .then(function() { - function addPriv(priv) { - return priv.reduce(function(promise, privName) { + function addPrivilege(privileges: string[]) { + return privileges.reduce(function(promise: Promise, privilegeName: string) { // We have to use non-test-subject selectors because this markup is generated by ui-select. return promise - .then(() => self.addPrivilegeToRole(privName)) + .then(() => self.addPrivilegeToRole(privilegeName)) .then(() => PageObjects.common.sleep(250)); }, Promise.resolve()); } - return addPriv(userObj.elasticsearch.indices[0].privileges); + return addPrivilege(roleObj.elasticsearch.indices[0].privileges); }) - //clicking the Granted fields and removing the asterix + // clicking the Granted fields and removing the asterix .then(async function() { - function addGrantedField(field) { - return field.reduce(function(promise, fieldName) { + function addGrantedField(field: string[]) { + return field.reduce(function(promise: Promise, fieldName: string) { return promise .then(function() { return find.setValue('[data-test-subj="fieldInput0"] input', fieldName + '\n'); @@ -380,7 +476,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { }, Promise.resolve()); } - if (userObj.elasticsearch.indices[0].field_security) { + if (roleObj.elasticsearch.indices[0].field_security) { // Toggle FLS switch await testSubjects.click('restrictFieldsQuery0'); @@ -390,10 +486,10 @@ export function SecurityPageProvider({ getService, getPageObjects }) { 'div[data-test-subj="fieldInput0"] [title="Remove * from selection in this group"] svg.euiIcon' ) .then(function() { - return addGrantedField(userObj.elasticsearch.indices[0].field_security.grant); + return addGrantedField(roleObj.elasticsearch.indices[0].field_security!.grant!); }); } - }) //clicking save button + }) // clicking save button .then(async () => { log.debug('click save button'); await testSubjects.click('roleFormSaveButton'); @@ -404,7 +500,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { ); } - async selectRole(role) { + async selectRole(role: string) { const dropdown = await testSubjects.find('rolesDropdown'); const input = await dropdown.findByCssSelector('input'); await input.type(role); @@ -413,8 +509,8 @@ export function SecurityPageProvider({ getService, getPageObjects }) { await testSubjects.find(`roleOption-${role}`); } - deleteUser(username) { - let alertText; + deleteUser(username: string) { + let alertText: string; log.debug('Delete user ' + username); return find .clickByDisplayedLinkText(username) @@ -440,11 +536,6 @@ export function SecurityPageProvider({ getService, getPageObjects }) { return alertText; }); } - - async getPermissionDeniedMessage() { - const el = await find.displayedByCssSelector('span.kuiInfoPanelHeader__title'); - return await el.getVisibleText(); - } } return new SecurityPage(); } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts index 49f9b01c96895..d67f6bc946df2 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics_creation.ts @@ -287,10 +287,16 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( }, async setModelMemory(modelMemory: string) { - await mlCommon.setValueWithChecks('mlAnalyticsCreateJobFlyoutModelMemoryInput', modelMemory, { - clearWithKeyboard: true, + await retry.tryForTime(15 * 1000, async () => { + await mlCommon.setValueWithChecks( + 'mlAnalyticsCreateJobFlyoutModelMemoryInput', + modelMemory, + { + clearWithKeyboard: true, + } + ); + await this.assertModelMemoryValue(modelMemory); }); - await this.assertModelMemoryValue(modelMemory); }, async assertCreateIndexPatternSwitchExists() { diff --git a/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts index 28a7cbd2e3c30..c94c623e97279 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/policy_list.ts @@ -12,7 +12,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - describe('When on the Endpoint Policy List', function() { + // FLAKY: https://github.com/elastic/kibana/issues/66579 + describe.skip('When on the Endpoint Policy List', function() { this.tags(['ciGroup7']); before(async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/policy'); @@ -46,7 +47,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { expect(noItemsFoundMessage).to.equal('No items found'); }); - describe('and policies exists', () => { + xdescribe('and policies exists', () => { let policyInfo: PolicyTestResourceInfo; before(async () => { diff --git a/x-pack/test/saml_api_integration/fixtures/saml_provider/kibana.json b/x-pack/test/saml_api_integration/fixtures/saml_provider/kibana.json new file mode 100644 index 0000000000000..3cbd37e38bb2d --- /dev/null +++ b/x-pack/test/saml_api_integration/fixtures/saml_provider/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "saml_provider_plugin", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/saml_api_integration/fixtures/saml_provider/metadata.xml b/x-pack/test/saml_api_integration/fixtures/saml_provider/metadata.xml new file mode 100644 index 0000000000000..19a6c13264144 --- /dev/null +++ b/x-pack/test/saml_api_integration/fixtures/saml_provider/metadata.xml @@ -0,0 +1,41 @@ + + + + + + + + MIIDOTCCAiGgAwIBAgIVANNWkg9lzNiLqNkMFhFKHcXyaZmqMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMCAXDTE5MTIyNzE3MDM0MloYDzIwNjkxMjE0MTcwMzQyWjARMQ8w +DQYDVQQDEwZraWJhbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQ +wYYbQtbRBKJ4uNZc2+IgRU+7NNL21ZebQlEIMgK7jAqOMrsW2b5DATz41Fd+GQFU +FUYYjwo+PQj6sJHshOJo/gNb32HrydvMI7YPvevkszkuEGCfXxQ3Dw2RTACLgD0Q +OCkwHvn3TMf0loloV/ePGWaZDYZaXi3a5DdWi/HFFoJysgF0JV2f6XyKhJkGaEfJ +s9pWX269zH/XQvGNx4BEimJpYB8h4JnDYPFIiQdqj+sl2b+kS1hH9kL5gBAMXjFU +vcNnX+PmyTjyJrGo75k0ku+spBf1bMwuQt3uSmM+TQIXkvFDmS0DOVESrpA5EC1T +BUGRz6o/I88Xx4Mud771AgMBAAGjYzBhMB0GA1UdDgQWBBQLB1Eo23M3Ss8MsFaz +V+Twcb3PmDAfBgNVHSMEGDAWgBQa7SYOe8NGcF00EbwPHA91YCsHSTAUBgNVHREE +DTALgglsb2NhbGhvc3QwCQYDVR0TBAIwADANBgkqhkiG9w0BAQsFAAOCAQEAnEl/ +z5IElIjvkK4AgMPrNcRlvIGDt2orEik7b6Jsq6/RiJQ7cSsYTZf7xbqyxNsUOTxv ++frj47MEN448H2nRvUxH29YR3XygV5aEwADSAhwaQWn0QfWTCZbJTmSoNEDtDOzX +TGDlAoCD9s9Xz9S1JpxY4H+WWRZrBSDM6SC1c6CzuEeZRuScNAjYD5mh2v6fOlSy +b8xJWSg0AFlJPCa3ZsA2SKbNqI0uNfJTnkXRm88Z2NHcgtlADbOLKauWfCrpgsCk +cZgo6yAYkOM148h/8wGla1eX+iE1R72NUABGydu8MSQKvc0emWJkGsC1/KqPlf/O +eOUsdwn1yDKHRxDHyA== + + + + + + + + + + diff --git a/x-pack/test/saml_api_integration/fixtures/saml_provider/server/index.ts b/x-pack/test/saml_api_integration/fixtures/saml_provider/server/index.ts new file mode 100644 index 0000000000000..7c3bc5d032160 --- /dev/null +++ b/x-pack/test/saml_api_integration/fixtures/saml_provider/server/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializer } from '../../../../../../src/core/server'; +import { initRoutes } from './init_routes'; + +export const plugin: PluginInitializer = () => ({ + setup: core => initRoutes(core), + start: () => {}, + stop: () => {}, +}); diff --git a/x-pack/test/saml_api_integration/fixtures/saml_provider/server/init_routes.ts b/x-pack/test/saml_api_integration/fixtures/saml_provider/server/init_routes.ts new file mode 100644 index 0000000000000..5777aa3f423f0 --- /dev/null +++ b/x-pack/test/saml_api_integration/fixtures/saml_provider/server/init_routes.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreSetup } from '../../../../../../src/core/server'; +import { getSAMLResponse, getSAMLRequestId } from '../../saml_tools'; + +export function initRoutes(core: CoreSetup) { + const serverInfo = core.http.getServerInfo(); + core.http.resources.register( + { + path: '/saml_provider/login', + validate: false, + options: { authRequired: false }, + }, + async (context, request, response) => { + const samlResponse = await getSAMLResponse({ + inResponseTo: await getSAMLRequestId(request.url.href!), + destination: `${serverInfo.protocol}://${serverInfo.host}:${serverInfo.port}/api/security/saml/callback`, + }); + + return response.renderHtml({ + body: ` + + Kibana SAML Login + + + +
+ +
+ + `, + }); + } + ); + + core.http.resources.register( + { path: '/saml_provider/login/submit.js', validate: false, options: { authRequired: false } }, + (context, request, response) => { + return response.renderJs({ body: 'document.getElementById("loginForm").submit();' }); + } + ); +}