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/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/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index d12d9de8c7dd4..1a756b205691c 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'; @@ -213,13 +212,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 +229,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/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 98ec4495cef29..f3297f21c572a 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 @@ -299,6 +299,13 @@ export class IndexPattern implements IIndexPattern { } async popularizeField(fieldName: string, unit = 1) { + /** + * This function is just used by Discover and it's high likely to be removed in the near future + * It doesn't use the save function to skip the error message that's displayed when + * a user adds several columns in a higher frequency that the changes can be persisted to ES + * resulting in 409 errors + */ + if (!this.id) return; const field = this.fields.getByName(fieldName); if (!field) { return; @@ -308,7 +315,15 @@ export class IndexPattern implements IIndexPattern { return; } field.count = count; - await this.save(); + + try { + const res = await this.savedObjectsClient.update(type, this.id, this.prepBody(), { + version: this.version, + }); + this.version = res._version; + } catch (e) { + // no need for an error message here + } } getNonScriptedFields() { 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/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 { 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/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/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts index 1eeebc8543d72..2c0d73f68760d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts @@ -16,11 +16,15 @@ import { AGENT, URL, USER, - CUSTOM_ERROR + CUSTOM_ERROR, + TRACE, + TRANSACTION } from '../sections'; export const ERROR_METADATA_SECTIONS: Section[] = [ { ...LABELS, required: true }, + TRACE, + TRANSACTION, ERROR, HTTP, HOST, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts index 5a83a9bf4ef9e..f8d14707f164d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts @@ -17,9 +17,9 @@ import { export const SPAN_METADATA_SECTIONS: Section[] = [ LABELS, - SPAN, - TRANSACTION, TRACE, + TRANSACTION, + SPAN, SERVICE, MESSAGE_SPAN, AGENT diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts index 3a9a21356ba35..69d934e588e9d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts @@ -20,11 +20,13 @@ import { USER, USER_AGENT, CUSTOM_TRANSACTION, - MESSAGE_TRANSACTION + MESSAGE_TRANSACTION, + TRACE } from '../sections'; export const TRANSACTION_METADATA_SECTIONS: Section[] = [ { ...LABELS, required: true }, + TRACE, TRANSACTION, HTTP, HOST, diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts index d6c0a72b8bb08..a41651a454b35 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts @@ -118,7 +118,8 @@ export const TRACE: Section = { key: 'trace', label: i18n.translate('xpack.apm.metadataTable.section.traceLabel', { defaultMessage: 'Trace' - }) + }), + properties: ['id'] }; export const ERROR: Section = { diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 474ab1c6082a4..782f8957cf188 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import Boom from 'boom'; import { unique } from 'lodash'; -import { ScopedAnnotationsClient } from '../../../observability/server'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAgentName } from '../lib/services/get_service_agent_name'; import { getServices } from '../lib/services/get_services'; @@ -95,13 +94,10 @@ export const serviceAnnotationsRoute = createRoute(() => ({ const { serviceName } = context.params.path; const { environment } = context.params.query; - let annotationsClient: ScopedAnnotationsClient | undefined; - - if (context.plugins.observability) { - annotationsClient = await context.plugins.observability.getScopedAnnotationsClient( - request - ); - } + const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient( + context, + request + ); return getServiceAnnotations({ setup, @@ -143,6 +139,7 @@ export const serviceAnnotationsCreateRoute = createRoute(() => ({ }, handler: async ({ request, context }) => { const annotationsClient = await context.plugins.observability?.getScopedAnnotationsClient( + context, request ); 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/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 002b58a4e827a..53df2cf6ce219 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 @@ -475,7 +475,6 @@ export class IndexTable extends Component { showHiddenIndicesChanged(event.target.checked)} label={ 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 6c0d376922b62..1f62680a41cbc 100644 --- a/x-pack/plugins/index_management/server/lib/fetch_indices.ts +++ b/x-pack/plugins/index_management/server/lib/fetch_indices.ts @@ -38,17 +38,12 @@ async function fetchIndicesCall( callAsCurrentUser: CallAsCurrentUser, indexNames?: string[] ): Promise { - let indexNamesCSV: string; - if (indexNames && indexNames.length) { - indexNamesCSV = indexNames.join(','); - } else { - indexNamesCSV = '*'; - } + 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: `/${indexNamesCSV}`, + path: `/${indexNamesString}`, query: { expand_wildcards: 'hidden,all', }, @@ -65,7 +60,7 @@ async function fetchIndicesCall( format: 'json', h: 'health,status,index,uuid,pri,rep,docs.count,sth,store.size', expand_wildcards: 'hidden,all', - index: indexNamesCSV, + index: indexNamesString, }; // This call retrieves health and other high-level information about indices. 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/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/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 7dc39225f780f..0608c978ad0dc 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -104,7 +104,12 @@ describe('Lens App', () => { storage: Storage; docId?: string; docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; + redirectTo: ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => void; originatingApp: string | undefined; }> { return ({ @@ -140,7 +145,14 @@ describe('Lens App', () => { load: jest.fn(), save: jest.fn(), }, - redirectTo: jest.fn((id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => {}), + redirectTo: jest.fn( + ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => {} + ), } as unknown) as jest.Mocked<{ navigation: typeof navigationStartMock; editorFrame: EditorFrameInstance; @@ -149,7 +161,12 @@ describe('Lens App', () => { storage: Storage; docId?: string; docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; + redirectTo: ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => void; originatingApp: string | undefined; }>; } @@ -348,7 +365,12 @@ describe('Lens App', () => { storage: Storage; docId?: string; docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; + redirectTo: ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => void; originatingApp: string | undefined; }>; @@ -521,7 +543,7 @@ describe('Lens App', () => { expression: 'kibana 3', }); - expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); + expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, undefined, true); inst.setProps({ docId: 'aaa' }); @@ -541,7 +563,7 @@ describe('Lens App', () => { expression: 'kibana 3', }); - expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, true); + expect(args.redirectTo).toHaveBeenCalledWith('aaa', undefined, undefined, true); inst.setProps({ docId: 'aaa' }); @@ -609,7 +631,7 @@ describe('Lens App', () => { title: 'hello there', }); - expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, true); + expect(args.redirectTo).toHaveBeenCalledWith('aaa', true, undefined, true); }); it('saves app filters and does not save pinned filters', async () => { @@ -677,7 +699,12 @@ describe('Lens App', () => { storage: Storage; docId?: string; docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; + redirectTo: ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => void; }>; beforeEach(() => { diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index 9d0df16c68555..718f49413a082 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -33,6 +33,7 @@ interface State { isLoading: boolean; isSaveModalVisible: boolean; indexPatternsForTopNav: IndexPatternInstance[]; + originatingApp: string | undefined; persistedDoc?: Document; lastKnownDoc?: Document; @@ -54,7 +55,7 @@ export function App({ docId, docStorage, redirectTo, - originatingApp, + originatingAppFromUrl, navigation, }: { editorFrame: EditorFrameInstance; @@ -64,8 +65,13 @@ export function App({ storage: IStorageWrapper; docId?: string; docStorage: SavedObjectStore; - redirectTo: (id?: string, returnToOrigin?: boolean, newlyCreated?: boolean) => void; - originatingApp?: string | undefined; + redirectTo: ( + id?: string, + returnToOrigin?: boolean, + originatingApp?: string | undefined, + newlyCreated?: boolean + ) => void; + originatingAppFromUrl?: string | undefined; }) { const language = storage.get('kibana.userQueryLanguage') || core.uiSettings.get('search:queryLanguage'); @@ -77,6 +83,7 @@ export function App({ isSaveModalVisible: false, indexPatternsForTopNav: [], query: { query: '', language }, + originatingApp: originatingAppFromUrl, dateRange: { fromDate: currentRange.from, toDate: currentRange.to, @@ -229,7 +236,7 @@ export function App({ lastKnownDoc: newDoc, })); if (docId !== id || saveProps.returnToOrigin) { - redirectTo(id, saveProps.returnToOrigin, newlyCreated); + redirectTo(id, saveProps.returnToOrigin, state.originatingApp, newlyCreated); } }) .catch(e => { @@ -269,7 +276,7 @@ export function App({
{ if (isSaveable && lastKnownDoc) { setState(s => ({ ...s, isSaveModalVisible: true })); @@ -422,7 +429,7 @@ export function App({
{lastKnownDoc && state.isSaveModalVisible && ( runSave(props)} onClose={() => setState(s => ({ ...s, isSaveModalVisible: false }))} documentInfo={{ diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index b49e6cf5ba7b9..7c875935f6320 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -47,15 +47,11 @@ export async function mountApp( ); const redirectTo = ( routeProps: RouteComponentProps<{ id?: string }>, - originatingApp: string, id?: string, returnToOrigin?: boolean, + originatingApp?: string, newlyCreated?: boolean ) => { - if (!!originatingApp && !returnToOrigin) { - removeQueryParam(routeProps.history, 'embeddableOriginatingApp'); - } - if (!id) { routeProps.history.push('/'); } else if (!originatingApp) { @@ -78,7 +74,10 @@ export async function mountApp( const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { trackUiEvent('loaded'); const urlParams = parse(routeProps.location.search) as Record; - const originatingApp = urlParams.embeddableOriginatingApp; + const originatingAppFromUrl = urlParams.embeddableOriginatingApp; + if (urlParams.embeddableOriginatingApp) { + removeQueryParam(routeProps.history, 'embeddableOriginatingApp'); + } return ( - redirectTo(routeProps, originatingApp, id, returnToOrigin, newlyCreated) + redirectTo={(id, returnToOrigin, originatingApp, newlyCreated) => + redirectTo(routeProps, id, returnToOrigin, originatingApp, newlyCreated) } - originatingApp={originatingApp} + originatingAppFromUrl={originatingAppFromUrl} /> ); }; 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/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/observability/kibana.json b/x-pack/plugins/observability/kibana.json index 8e2cfe980039c..712a46f76bb74 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,6 +6,9 @@ "xpack", "observability" ], + "optionalPlugins": [ + "licensing" + ], "ui": true, "server": true } diff --git a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts index 58639ef084ce6..6ea9f82d11ab9 100644 --- a/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts +++ b/x-pack/plugins/observability/server/lib/annotations/bootstrap_annotations.ts @@ -3,7 +3,12 @@ * 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, PluginInitializerContext, KibanaRequest } from 'kibana/server'; +import { + CoreSetup, + PluginInitializerContext, + KibanaRequest, + RequestHandlerContext, +} from 'kibana/server'; import { PromiseReturnType } from '../../../typings/common'; import { createAnnotationsClient } from './create_annotations_client'; import { registerAnnotationAPIs } from './register_annotation_apis'; @@ -31,11 +36,12 @@ export async function bootstrapAnnotations({ index, core, context }: Params) { }); return { - getScopedAnnotationsClient: (request: KibanaRequest) => { + getScopedAnnotationsClient: (requestContext: RequestHandlerContext, request: KibanaRequest) => { return createAnnotationsClient({ index, apiCaller: core.elasticsearch.dataClient.asScoped(request).callAsCurrentUser, logger, + license: requestContext.licensing?.license, }); }, }; diff --git a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts index 3f2604468e17c..71b1a42b2000d 100644 --- a/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts +++ b/x-pack/plugins/observability/server/lib/annotations/create_annotations_client.ts @@ -7,6 +7,8 @@ import { APICaller, Logger } from 'kibana/server'; import * as t from 'io-ts'; import { Client } from 'elasticsearch'; +import Boom from 'boom'; +import { ILicense } from '../../../../licensing/server'; import { createAnnotationRt, deleteAnnotationRt, @@ -40,8 +42,9 @@ export function createAnnotationsClient(params: { index: string; apiCaller: APICaller; logger: Logger; + license?: ILicense; }) { - const { index, apiCaller, logger } = params; + const { index, apiCaller, logger, license } = params; const initIndex = () => createOrUpdateIndex({ @@ -51,48 +54,59 @@ export function createAnnotationsClient(params: { logger, }); + function ensureGoldLicense any>(fn: T): T { + return ((...args) => { + if (!license?.hasAtLeast('gold')) { + throw Boom.forbidden('Annotations require at least a gold license or a trial license.'); + } + return fn(...args); + }) as T; + } + return { get index() { return index; }, - create: async ( - createParams: CreateParams - ): Promise<{ _id: string; _index: string; _source: Annotation }> => { - const indexExists = await apiCaller('indices.exists', { - index, - }); + create: ensureGoldLicense( + async ( + createParams: CreateParams + ): Promise<{ _id: string; _index: string; _source: Annotation }> => { + const indexExists = await apiCaller('indices.exists', { + index, + }); - if (!indexExists) { - await initIndex(); - } + if (!indexExists) { + await initIndex(); + } - const annotation = { - ...createParams, - event: { - created: new Date().toISOString(), - }, - }; + const annotation = { + ...createParams, + event: { + created: new Date().toISOString(), + }, + }; - const response = (await apiCaller('index', { - index, - body: annotation, - refresh: 'wait_for', - })) as IndexDocumentResponse; + const response = (await apiCaller('index', { + index, + body: annotation, + refresh: 'wait_for', + })) as IndexDocumentResponse; - return apiCaller('get', { - index, - id: response._id, - }); - }, - getById: async (getByIdParams: GetByIdParams) => { + return apiCaller('get', { + index, + id: response._id, + }); + } + ), + getById: ensureGoldLicense(async (getByIdParams: GetByIdParams) => { const { id } = getByIdParams; return apiCaller('get', { id, index, }); - }, - delete: async (deleteParams: DeleteParams) => { + }), + delete: ensureGoldLicense(async (deleteParams: DeleteParams) => { const { id } = deleteParams; const response = (await apiCaller('delete', { @@ -101,6 +115,6 @@ export function createAnnotationsClient(params: { refresh: 'wait_for', })) as PromiseReturnType; return response; - }, + }), }; } diff --git a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts index 3c29822acd6dd..21ebfcd6205e7 100644 --- a/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts +++ b/x-pack/plugins/observability/server/lib/annotations/register_annotation_apis.ts @@ -32,7 +32,7 @@ export function registerAnnotationAPIs({ handler: (params: { data: t.TypeOf; client: ScopedAnnotationsClient }) => Promise ): RequestHandler { return async (...args: Parameters) => { - const [, request, response] = args; + const [context, request, response] = args; const rt = types; @@ -56,16 +56,26 @@ export function registerAnnotationAPIs({ index, apiCaller, logger, + license: context.licensing?.license, }); - const res = await handler({ - data: validation.right, - client, - }); + try { + const res = await handler({ + data: validation.right, + client, + }); - return response.ok({ - body: res, - }); + return response.ok({ + body: res, + }); + } catch (err) { + return response.custom({ + statusCode: err.output?.statusCode ?? 500, + body: { + message: err.output?.payload?.message ?? 'An internal server error occured', + }, + }); + } }; } 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 abc54183ca41a..e49cfbbc67d61 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13131,7 +13131,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 b86d137f0692d..1d3eb0e3a0da8 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13138,7 +13138,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/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index c4464ff575218..6aa9d1aa3c645 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -12,12 +12,10 @@ import { updateState } from './common'; import { ACTION_GROUP_DEFINITIONS, DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; import { Cert, CertResult } from '../../../common/runtime_types'; import { commonStateTranslations, tlsTranslations } from './translations'; +import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs'; const { TLS } = ACTION_GROUP_DEFINITIONS; -const DEFAULT_FROM = 'now-1d'; -const DEFAULT_TO = 'now'; -const DEFAULT_INDEX = 0; const DEFAULT_SIZE = 20; interface TlsAlertState { @@ -113,7 +111,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ dynamicSettings, from: DEFAULT_FROM, to: DEFAULT_TO, - index: DEFAULT_INDEX, + index: 0, size: DEFAULT_SIZE, notValidAfter: `now+${dynamicSettings?.certExpirationThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index 689dce98859e1..5fa5c331d398e 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -180,7 +180,7 @@ describe('getCerts', () => { }, Object { "range": Object { - "@timestamp": Object { + "monitor.timespan": Object { "gte": "now-2d", "lte": "now+1h", }, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts index 57a59936ddf7c..4793d420cbfd8 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_certs.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_certs.ts @@ -51,7 +51,7 @@ export const getCerts: UMElasticsearchQueryFn = asyn }, { range: { - '@timestamp': { + 'monitor.timespan': { gte: from, lte: to, }, diff --git a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts index a5ca6e264d299..fb22d603a2d56 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts @@ -9,10 +9,10 @@ import { API_URLS } from '../../../common/constants'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; -const DEFAULT_INDEX = 0; +export const DEFAULT_FROM = 'now-5m'; +export const DEFAULT_TO = 'now'; + const DEFAULT_SIZE = 25; -const DEFAULT_FROM = 'now-1d'; -const DEFAULT_TO = 'now'; const DEFAULT_SORT = 'not_after'; const DEFAULT_DIRECTION = 'asc'; @@ -31,7 +31,7 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = }), }, handler: async ({ callES, dynamicSettings }, _context, request, response): Promise => { - const index = request.query?.index ?? DEFAULT_INDEX; + const index = request.query?.index ?? 0; const size = request.query?.size ?? DEFAULT_SIZE; const from = request.query?.from ?? DEFAULT_FROM; const to = request.query?.to ?? DEFAULT_TO; diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 3ec7776f848af..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 = [ @@ -20,6 +21,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/basic/config.ts'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), + require.resolve('../test/apm_api_integration/basic/config.ts'), + require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/basic/config.ts'), require.resolve('../test/plugin_api_integration/config.ts'), @@ -29,6 +32,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/token_api_integration/config.js'), require.resolve('../test/oidc_api_integration/config.ts'), require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), + require.resolve('../test/observability_api_integration/basic/config.ts'), + require.resolve('../test/observability_api_integration/trial/config.ts'), require.resolve('../test/pki_api_integration/config.ts'), require.resolve('../test/login_selector_api_integration/config.ts'), require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 75fa90bb4c3fe..321fbce52a75a 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -23,7 +23,6 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./uptime')); loadTestFile(require.resolve('./maps')); - loadTestFile(require.resolve('./apm')); loadTestFile(require.resolve('./siem')); loadTestFile(require.resolve('./short_urls')); loadTestFile(require.resolve('./lens')); @@ -31,6 +30,5 @@ export default function({ loadTestFile }) { loadTestFile(require.resolve('./ingest')); loadTestFile(require.resolve('./endpoint')); loadTestFile(require.resolve('./ml')); - loadTestFile(require.resolve('./observability')); }); } diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts index 908c571e07e06..f9bea050293fc 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_ping.ts @@ -18,6 +18,7 @@ export const makePing = async ( refresh: boolean = true, tls: boolean | TlsProps = false ) => { + const timestamp = new Date(); const baseDoc: any = { tcp: { rtt: { @@ -40,7 +41,7 @@ export const makePing = async ( ephemeral_id: '0d9a8dc6-f604-49e3-86a0-d8f9d6f2cbad', version: '8.0.0', }, - '@timestamp': new Date().toISOString(), + '@timestamp': timestamp.toISOString(), resolve: { rtt: { us: 350, @@ -88,6 +89,10 @@ export const makePing = async ( check_group: uuid.v4(), type: 'http', status: 'up', + timespan: { + gte: timestamp.toISOString(), + lt: new Date(timestamp.getTime() + 5000).toISOString, + }, }, event: { dataset: 'uptime', diff --git a/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts index 3606462522024..477c9857ca363 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/helper/make_tls.ts @@ -39,7 +39,7 @@ export const makeTls = ({ valid = true, commonName = '*.elastic.co', expiry, sha server: { x509: { not_before: '2020-03-01T00:00:00.000Z', - not_after: '2020-05-30T12:00:00.000Z', + not_after: expiryDate, issuer: { distinguished_name: 'CN=DigiCert SHA2 High Assurance Server CA,OU=www.digicert.com,O=DigiCert Inc,C=US', diff --git a/x-pack/test/apm_api_integration/basic/config.ts b/x-pack/test/apm_api_integration/basic/config.ts new file mode 100644 index 0000000000000..541fe9ec023bc --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/config.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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig({ + license: 'basic', + name: 'X-Pack APM API integration tests (basic)', + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts similarity index 98% rename from x-pack/test/api_integration/apis/apm/agent_configuration.ts rename to x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts index 8af648e062cf4..04974d57074a5 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/apm_api_integration/basic/tests/agent_configuration.ts @@ -6,8 +6,9 @@ import expect from '@kbn/expect'; import { AgentConfigurationIntake } from '../../../../plugins/apm/common/agent_configuration/configuration_types'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +// eslint-disable-next-line import/no-default-export export default function agentConfigurationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const log = getService('log'); diff --git a/x-pack/test/apm_api_integration/basic/tests/annotations.ts b/x-pack/test/apm_api_integration/basic/tests/annotations.ts new file mode 100644 index 0000000000000..a939ef0627356 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/annotations.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function annotationApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + function request({ method, url, data }: { method: string; url: string; data?: JsonObject }) { + switch (method.toLowerCase()) { + case 'post': + return supertest + .post(url) + .send(data) + .set('kbn-xsrf', 'foo'); + + default: + throw new Error(`Unsupported methoed ${method}`); + } + } + + describe('APM annotations with a basic license', () => { + describe('when creating an annotation', () => { + it('fails with a 403 forbidden', async () => { + const response = await request({ + url: '/api/apm/services/opbeans-java/annotation', + method: 'POST', + data: { + '@timestamp': new Date().toISOString(), + message: 'New deployment', + tags: ['foo'], + service: { + version: '1.1', + environment: 'production', + }, + }, + }); + + expect(response.status).to.be(403); + expect(response.body.message).to.be( + 'Annotations require at least a gold license or a trial license.' + ); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/apm/custom_link.ts b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts similarity index 96% rename from x-pack/test/api_integration/apis/apm/custom_link.ts rename to x-pack/test/apm_api_integration/basic/tests/custom_link.ts index 8aefadd811775..910c4797f39b7 100644 --- a/x-pack/test/api_integration/apis/apm/custom_link.ts +++ b/x-pack/test/apm_api_integration/basic/tests/custom_link.ts @@ -3,13 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// import querystring from 'querystring'; -// import {isEmpty} from 'lodash' import URL from 'url'; import expect from '@kbn/expect'; import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +// eslint-disable-next-line import/no-default-export export default function customLinksTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const log = getService('log'); diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts similarity index 98% rename from x-pack/test/api_integration/apis/apm/feature_controls.ts rename to x-pack/test/apm_api_integration/basic/tests/feature_controls.ts index 5f61c963a69aa..f3647c65106c9 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/apm_api_integration/basic/tests/feature_controls.ts @@ -5,8 +5,9 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +// eslint-disable-next-line import/no-default-export export default function featureControlsTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts similarity index 73% rename from x-pack/test/api_integration/apis/apm/index.ts rename to x-pack/test/apm_api_integration/basic/tests/index.ts index de076e8c46729..f3c1a3c3f63d5 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -3,11 +3,13 @@ * 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 '../../common/ftr_provider_context'; -import { FtrProviderContext } from '../../ftr_provider_context'; - +// eslint-disable-next-line import/no-default-export export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('APM specs', () => { + describe('APM specs (basic)', function() { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts new file mode 100644 index 0000000000000..9e011a98bbfcd --- /dev/null +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +interface Settings { + license: 'basic' | 'trial'; + testFiles: string[]; + name: string; +} + +export function createTestConfig(settings: Settings) { + const { testFiles, license, name } = settings; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackAPITestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + return { + testFiles, + servers: xPackAPITestsConfig.get('servers'), + services: xPackAPITestsConfig.get('services'), + junit: { + reportName: name, + }, + + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + license, + }, + kbnTestServer: xPackAPITestsConfig.get('kbnTestServer'), + }; + }; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts b/x-pack/test/apm_api_integration/common/ftr_provider_context.ts similarity index 53% rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts rename to x-pack/test/apm_api_integration/common/ftr_provider_context.ts index 787791f985c7d..90600816d1711 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts +++ b/x-pack/test/apm_api_integration/common/ftr_provider_context.ts @@ -4,7 +4,4 @@ * 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`; +export { FtrProviderContext } from '../../api_integration/ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/trial/config.ts b/x-pack/test/apm_api_integration/trial/config.ts new file mode 100644 index 0000000000000..ca5b11d469c47 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/config.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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig({ + license: 'trial', + name: 'X-Pack APM API integration tests (trial)', + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/api_integration/apis/apm/annotations.ts b/x-pack/test/apm_api_integration/trial/tests/annotations.ts similarity index 98% rename from x-pack/test/api_integration/apis/apm/annotations.ts rename to x-pack/test/apm_api_integration/trial/tests/annotations.ts index 4746a7713f34b..9beea6a53dd4d 100644 --- a/x-pack/test/api_integration/apis/apm/annotations.ts +++ b/x-pack/test/apm_api_integration/trial/tests/annotations.ts @@ -7,10 +7,11 @@ import expect from '@kbn/expect'; import { merge, cloneDeep, isPlainObject } from 'lodash'; import { JsonObject } from 'src/plugins/kibana_utils/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; +// eslint-disable-next-line import/no-default-export export default function annotationApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); @@ -42,7 +43,7 @@ export default function annotationApiTests({ getService }: FtrProviderContext) { } } - describe('APM annotations', () => { + describe('APM annotations with a trial license', () => { describe('when creating an annotation', () => { afterEach(async () => { const indexExists = (await es.indices.exists({ index: DEFAULT_INDEX_NAME })).body; diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts new file mode 100644 index 0000000000000..3a4571afb3d4a --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/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 '../../../api_integration/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('APM specs (trial)', function() { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./annotations')); + }); +} 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/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index b5946dcf22610..f36f26f615125 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -60,10 +60,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_advanced_settings_all_role'), security.user.delete('global_advanced_settings_all_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 71c10bd8248be..57f198d204764 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -57,10 +57,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_canvas_all_role'), security.user.delete('global_canvas_all_user'), - PageObjects.security.forceLogout(), ]); }); 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/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index 25ef24f6bfd39..008590c9c8dc1 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -62,10 +62,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_index_patterns_all_role'), security.user.delete('global_index_patterns_all_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts index ede77b7d9afa7..1520621312b08 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -52,10 +52,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_infrastructure_all_role'), security.user.delete('global_infrastructure_all_user'), - PageObjects.security.forceLogout(), ]); }); @@ -168,10 +168,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_infrastructure_read_role'), security.user.delete('global_infrastructure_read_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 48ad4e90fd413..f53f60e5c7ea7 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -49,10 +49,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_all_role'), security.user.delete('global_logs_all_user'), - PageObjects.security.forceLogout(), ]); }); @@ -112,10 +112,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_read_role'), security.user.delete('global_logs_read_user'), - PageObjects.security.forceLogout(), ]); }); @@ -175,10 +175,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_logs_no_privileges_role'), security.user.delete('global_logs_no_privileges_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index ece162cbd96cc..1651b118ea88c 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -57,10 +57,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_maps_all_role'), security.user.delete('global_maps_all_user'), - PageObjects.security.forceLogout(), ]); }); diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index d75540ae0b4a9..9fafaa6aae16b 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -52,10 +52,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_all_role'), security.user.delete('global_all_user'), - PageObjects.security.forceLogout(), ]); }); @@ -170,10 +170,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_som_read_role'), security.user.delete('global_som_read_user'), - PageObjects.security.forceLogout(), ]); }); @@ -293,10 +293,10 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); after(async () => { + await PageObjects.security.forceLogout(); await Promise.all([ security.role.delete('global_visualize_all_role'), security.user.delete('global_visualize_all_user'), - PageObjects.security.forceLogout(), ]); }); 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/uptime/certificates.ts b/x-pack/test/functional/apps/uptime/certificates.ts index 93a8a852294b2..4a10955637844 100644 --- a/x-pack/test/functional/apps/uptime/certificates.ts +++ b/x-pack/test/functional/apps/uptime/certificates.ts @@ -14,13 +14,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/65010 - describe.skip('certificate page', function() { - before(async () => { - await uptime.goToRoot(true); - }); - + describe('certificates', function() { beforeEach(async () => { + await uptime.goToRoot(true); await makeCheck({ es, tls: true }); await uptimeService.navigation.refreshApp(); }); @@ -30,33 +26,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await uptimeService.navigation.goToCertificates(); }); - it('displays certificates', async () => { - await uptimeService.cert.hasCertificates(); - }); + describe('page', () => { + beforeEach(async () => { + await uptimeService.navigation.goToCertificates(); + }); - it('displays specific certificates', async () => { - const certId = getSha256(); - const { monitorId } = await makeCheck({ - es, - tls: { - sha256: certId, - }, + it('displays certificates', async () => { + await uptimeService.cert.hasCertificates(); }); - await uptimeService.navigation.refreshApp(); - await uptimeService.cert.certificateExists({ certId, monitorId }); - }); + it('displays specific certificates', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); - it('performs search against monitor id', async () => { - const certId = getSha256(); - const { monitorId } = await makeCheck({ - es, - tls: { - sha256: certId, - }, + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.certificateExists({ certId, monitorId }); + }); + + it('performs search against monitor id', async () => { + const certId = getSha256(); + const { monitorId } = await makeCheck({ + es, + tls: { + sha256: certId, + }, + }); + await uptimeService.navigation.refreshApp(); + await uptimeService.cert.searchIsWorking(monitorId); }); - await uptimeService.navigation.refreshApp(); - await uptimeService.cert.searchIsWorking(monitorId); }); }); }; 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_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/observability_api_integration/basic/config.ts b/x-pack/test/observability_api_integration/basic/config.ts new file mode 100644 index 0000000000000..0e8bf1daaf9e6 --- /dev/null +++ b/x-pack/test/observability_api_integration/basic/config.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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig({ + license: 'basic', + name: 'X-Pack Observability API integration tests (basic)', + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/observability_api_integration/basic/tests/annotations.ts b/x-pack/test/observability_api_integration/basic/tests/annotations.ts new file mode 100644 index 0000000000000..cd86c8a0f2cda --- /dev/null +++ b/x-pack/test/observability_api_integration/basic/tests/annotations.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { JsonObject } from 'src/plugins/kibana_utils/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function annotationApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + function request({ method, url, data }: { method: string; url: string; data?: JsonObject }) { + switch (method.toLowerCase()) { + case 'post': + return supertest + .post(url) + .send(data) + .set('kbn-xsrf', 'foo'); + + default: + throw new Error(`Unsupported methoed ${method}`); + } + } + + describe('Observability annotations with a basic license', () => { + describe('when creating an annotation', () => { + it('fails with a 403 forbidden', async () => { + const response = await request({ + url: '/api/observability/annotation', + method: 'POST', + data: { + annotation: { + type: 'deployment', + }, + '@timestamp': new Date().toISOString(), + message: 'test message', + tags: ['apm'], + }, + }); + + expect(response.status).to.be(403); + expect(response.body.message).to.be( + 'Annotations require at least a gold license or a trial license.' + ); + }); + }); + }); +} diff --git a/x-pack/test/observability_api_integration/basic/tests/index.ts b/x-pack/test/observability_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..a4c04a9229fa9 --- /dev/null +++ b/x-pack/test/observability_api_integration/basic/tests/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 '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Observability specs (basic)', function() { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./annotations')); + }); +} diff --git a/x-pack/test/observability_api_integration/common/config.ts b/x-pack/test/observability_api_integration/common/config.ts new file mode 100644 index 0000000000000..9e011a98bbfcd --- /dev/null +++ b/x-pack/test/observability_api_integration/common/config.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +interface Settings { + license: 'basic' | 'trial'; + testFiles: string[]; + name: string; +} + +export function createTestConfig(settings: Settings) { + const { testFiles, license, name } = settings; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackAPITestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + return { + testFiles, + servers: xPackAPITestsConfig.get('servers'), + services: xPackAPITestsConfig.get('services'), + junit: { + reportName: name, + }, + + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + license, + }, + kbnTestServer: xPackAPITestsConfig.get('kbnTestServer'), + }; + }; +} diff --git a/x-pack/test/observability_api_integration/common/ftr_provider_context.ts b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts new file mode 100644 index 0000000000000..90600816d1711 --- /dev/null +++ b/x-pack/test/observability_api_integration/common/ftr_provider_context.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { FtrProviderContext } from '../../api_integration/ftr_provider_context'; diff --git a/x-pack/test/observability_api_integration/trial/config.ts b/x-pack/test/observability_api_integration/trial/config.ts new file mode 100644 index 0000000000000..c073e2e6af7fe --- /dev/null +++ b/x-pack/test/observability_api_integration/trial/config.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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig({ + license: 'trial', + name: 'X-Pack Observability API integration tests (trial)', + testFiles: [require.resolve('./tests')], +}); diff --git a/x-pack/test/api_integration/apis/observability/annotations.ts b/x-pack/test/observability_api_integration/trial/tests/annotations.ts similarity index 98% rename from x-pack/test/api_integration/apis/observability/annotations.ts rename to x-pack/test/observability_api_integration/trial/tests/annotations.ts index 6d32162bfcc65..ad3bcdbfabd8b 100644 --- a/x-pack/test/api_integration/apis/observability/annotations.ts +++ b/x-pack/test/observability_api_integration/trial/tests/annotations.ts @@ -8,10 +8,11 @@ import expect from '@kbn/expect'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { Annotation } from '../../../../plugins/observability/common/annotations'; import { ESSearchHit } from '../../../../plugins/apm/typings/elasticsearch'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; const DEFAULT_INDEX_NAME = 'observability-annotations'; +// eslint-disable-next-line import/no-default-export export default function annotationApiTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); diff --git a/x-pack/test/observability_api_integration/trial/tests/index.ts b/x-pack/test/observability_api_integration/trial/tests/index.ts new file mode 100644 index 0000000000000..d1acf4d98f7f9 --- /dev/null +++ b/x-pack/test/observability_api_integration/trial/tests/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 '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('Observability specs (trial)', function() { + this.tags('ciGroup1'); + loadTestFile(require.resolve('./annotations')); + }); +} 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();' }); + } + ); +}