diff --git a/.eslintrc.js b/.eslintrc.js index f1e0b7d9353e8..2edd9f5d433f2 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', 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/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/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 333aa93b7776d..e9cf8235ff3f4 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -28,7 +28,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const security = getService('security'); const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); - describe('visual builder', function describeIndexTests() { + // FLAKY: https://github.com/elastic/kibana/issues/43150 + describe.skip('visual builder', function describeIndexTests() { this.tags('includeFirefox'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); diff --git a/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/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 ( = 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/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/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index e9b736e379b58..2936eea21805d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -14,17 +14,6 @@ export { DATASOURCE_SAVED_OBJECT_TYPE, } from '../../../../common'; -export const BASE_PATH = '/app/ingestManager'; -export const EPM_PATH = '/epm'; -export const EPM_LIST_ALL_PACKAGES_PATH = EPM_PATH; -export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; -export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; -export const AGENT_CONFIG_PATH = '/configs'; -export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; -export const DATA_STREAM_PATH = '/data-streams'; -export const FLEET_PATH = '/fleet'; -export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; -export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; -export const FLEET_ENROLLMENT_TOKENS_PATH = `/fleet/enrollment-tokens`; +export * from './page_paths'; export const INDEX_NAME = '.kibana'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts new file mode 100644 index 0000000000000..73771fa3cb343 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/page_paths.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type StaticPage = + | 'overview' + | 'integrations' + | 'integrations_all' + | 'integrations_installed' + | 'configurations' + | 'configurations_list' + | 'fleet' + | 'fleet_enrollment_tokens' + | 'data_streams'; + +export type DynamicPage = + | 'integration_details' + | 'configuration_details' + | 'add_datasource_from_configuration' + | 'add_datasource_from_integration' + | 'edit_datasource' + | 'fleet_agent_list' + | 'fleet_agent_details'; + +export type Page = StaticPage | DynamicPage; + +export interface DynamicPagePathValues { + [key: string]: string; +} + +export const BASE_PATH = '/app/ingestManager'; + +// If routing paths are changed here, please also check to see if +// `pagePathGetters()`, below, needs any modifications +export const PAGE_ROUTING_PATHS = { + overview: '/', + integrations: '/integrations/:tabId?', + integrations_all: '/integrations', + integrations_installed: '/integrations/installed', + integration_details: '/integrations/detail/:pkgkey/:panel?', + configurations: '/configs', + configurations_list: '/configs', + configuration_details: '/configs/:configId/:tabId?', + configuration_details_yaml: '/configs/:configId/yaml', + configuration_details_settings: '/configs/:configId/settings', + add_datasource_from_configuration: '/configs/:configId/add-datasource', + add_datasource_from_integration: '/integrations/:pkgkey/add-datasource', + edit_datasource: '/configs/:configId/edit-datasource/:datasourceId', + fleet: '/fleet', + fleet_agent_list: '/fleet/agents', + fleet_agent_details: '/fleet/agents/:agentId/:tabId?', + fleet_agent_details_events: '/fleet/agents/:agentId', + fleet_agent_details_details: '/fleet/agents/:agentId/details', + fleet_enrollment_tokens: '/fleet/enrollment-tokens', + data_streams: '/data-streams', +}; + +export const pagePathGetters: { + [key in StaticPage]: () => string; +} & + { + [key in DynamicPage]: (values: DynamicPagePathValues) => string; + } = { + overview: () => '/', + integrations: () => '/integrations', + integrations_all: () => '/integrations', + integrations_installed: () => '/integrations/installed', + integration_details: ({ pkgkey, panel }) => + `/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`, + configurations: () => '/configs', + configurations_list: () => '/configs', + configuration_details: ({ configId, tabId }) => `/configs/${configId}${tabId ? `/${tabId}` : ''}`, + add_datasource_from_configuration: ({ configId }) => `/configs/${configId}/add-datasource`, + add_datasource_from_integration: ({ pkgkey }) => `/integrations/${pkgkey}/add-datasource`, + edit_datasource: ({ configId, datasourceId }) => + `/configs/${configId}/edit-datasource/${datasourceId}`, + fleet: () => '/fleet', + fleet_agent_list: ({ kuery }) => `/fleet/agents${kuery ? `?kuery=${kuery}` : ''}`, + fleet_agent_details: ({ agentId, tabId }) => + `/fleet/agents/${agentId}${tabId ? `/${tabId}` : ''}`, + fleet_enrollment_tokens: () => '/fleet/enrollment-tokens', + data_streams: () => '/data-streams', +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 66c7333150fb7..a752ad2a8912b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities'; export { useCore, CoreContext } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; +export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; export { usePackageIconType, UsePackageIconType } from './use_package_icon_type'; export { usePagination, Pagination } from './use_pagination'; @@ -15,3 +16,4 @@ export { useDebounce } from './use_debounce'; export * from './use_request'; export * from './use_input'; export * from './use_url_params'; +export * from './use_fleet_status'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx index ff6656e969c93..207c757fd5b16 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_breadcrumbs.tsx @@ -3,11 +3,225 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb } from 'src/core/public'; +import { BASE_PATH, Page, DynamicPagePathValues, pagePathGetters } from '../constants'; import { useCore } from './use_core'; -export function useBreadcrumbs(newBreadcrumbs: ChromeBreadcrumb[]) { - const { chrome } = useCore(); - return chrome.setBreadcrumbs(newBreadcrumbs); +const BASE_BREADCRUMB: ChromeBreadcrumb = { + href: pagePathGetters.overview(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.appTitle', { + defaultMessage: 'Ingest Manager', + }), +}; + +const breadcrumbGetters: { + [key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[]; +} = { + overview: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.overviewPageTitle', { + defaultMessage: 'Overview', + }), + }, + ], + integrations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + ], + integrations_all: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.allIntegrationsPageTitle', { + defaultMessage: 'All', + }), + }, + ], + integrations_installed: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.installedIntegrationsPageTitle', { + defaultMessage: 'Installed', + }), + }, + ], + integration_details: ({ pkgTitle }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { text: pkgTitle }, + ], + configurations: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configurations_list: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + ], + configuration_details: ({ configName }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { text: configName }, + ], + add_datasource_from_configuration: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + add_datasource_from_integration: ({ pkgTitle, pkgkey }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.integrations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.integrationsPageTitle', { + defaultMessage: 'Integrations', + }), + }, + { + href: pagePathGetters.integration_details({ pkgkey }), + text: pkgTitle, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.addDatasourcePageTitle', { + defaultMessage: 'Add data source', + }), + }, + ], + edit_datasource: ({ configName, configId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.configurations(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.configurationsPageTitle', { + defaultMessage: 'Configurations', + }), + }, + { + href: pagePathGetters.configuration_details({ configId }), + text: configName, + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.editDatasourcePageTitle', { + defaultMessage: 'Edit data source', + }), + }, + ], + fleet: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + ], + fleet_agent_list: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + ], + fleet_agent_details: ({ agentHost }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetAgentsPageTitle', { + defaultMessage: 'Agents', + }), + }, + { text: agentHost }, + ], + fleet_enrollment_tokens: () => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.fleet(), + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetPageTitle', { + defaultMessage: 'Fleet', + }), + }, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.fleetEnrollmentTokensPageTitle', { + defaultMessage: 'Enrollment tokens', + }), + }, + ], + data_streams: () => [ + BASE_BREADCRUMB, + { + text: i18n.translate('xpack.ingestManager.breadcrumbs.datastreamsPageTitle', { + defaultMessage: 'Data streams', + }), + }, + ], +}; + +export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) { + const { chrome, http } = useCore(); + const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map(breadcrumb => ({ + ...breadcrumb, + href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined, + })); + const docTitle: string[] = [...breadcrumbs] + .reverse() + .map(breadcrumb => breadcrumb.text as string); + chrome.docTitle.change(docTitle); + chrome.setBreadcrumbs(breadcrumbs); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts index f6c5b8bc03fce..58537b2075c16 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts @@ -6,9 +6,9 @@ import { useCore } from './'; -const BASE_PATH = '/app/kibana'; +const KIBANA_BASE_PATH = '/app/kibana'; export function useKibanaLink(path: string = '/') { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + return core.http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts index 333606cec8028..1b17c5cb0b1f3 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_link.ts @@ -4,10 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { BASE_PATH } from '../constants'; +import { + BASE_PATH, + StaticPage, + DynamicPage, + DynamicPagePathValues, + pagePathGetters, +} from '../constants'; import { useCore } from './'; -export function useLink(path: string = '/') { +const getPath = (page: StaticPage | DynamicPage, values: DynamicPagePathValues = {}): string => { + return values ? pagePathGetters[page](values) : pagePathGetters[page as StaticPage](); +}; + +export const useLink = () => { const core = useCore(); - return core.http.basePath.prepend(`${BASE_PATH}#${path}`); -} + return { + getPath, + getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => { + const path = getPath(page, values); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); + }, + }; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 3612497e723cd..f6a386314272f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -18,7 +18,7 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; +import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; @@ -174,42 +174,42 @@ const IngestManagerRoutes = ({ ...rest }) => { } return ( - + - - + + - + - + - + - + - + - - + + - + ); }; @@ -265,3 +265,8 @@ export function renderApp( ReactDOM.unmountComponentAtNode(element); }; } + +export const teardownIngestManager = (coreStart: CoreStart) => { + coreStart.chrome.docTitle.reset(); + coreStart.chrome.setBreadcrumbs([]); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index e9d7fcb1cf5c5..fbe7c736e2df4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { showSettings?: boolean; @@ -39,8 +38,8 @@ export const DefaultLayout: React.FunctionComponent = ({ section, children, }) => { + const { getHref } = useLink(); const { epm, fleet } = useConfig(); - const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); return ( @@ -60,7 +59,7 @@ export const DefaultLayout: React.FunctionComponent = ({ - + = ({ = ({ defaultMessage="Integrations" /> - + = ({ = ({ defaultMessage="Fleet" /> - + ( ({ count, agentConfigId }) => { - const FLEET_URI = useLink(FLEET_AGENTS_PATH); + const { getHref } = useLink(); const displayValue = ( ( /> ); return count > 0 ? ( - + {displayValue} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 46233fdb59509..577f08cdc3313 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,16 +17,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, sendCreateDatasource, useCore, useConfig, sendGetAgentStatus, } from '../../../hooks'; -import { useLinks as useEPMLinks } from '../../epm/hooks'; import { ConfirmDeployConfigModal } from '../components'; import { CreateDatasourcePageLayout } from './components'; import { CreateDatasourceFrom, DatasourceFormState } from './types'; @@ -48,6 +47,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { const { params: { configId, pkgkey }, } = useRouteMatch(); + const { getHref, getPath } = useLink(); const history = useHistory(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -95,32 +95,46 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { // Datasource validation state const [validationResults, setValidationResults] = useState(); + // Form state + const [formState, setFormState] = useState('INVALID'); + // Update package info method - const updatePackageInfo = (updatedPackageInfo: PackageInfo | undefined) => { - if (updatedPackageInfo) { - setPackageInfo(updatedPackageInfo); - setFormState('VALID'); - } else { - setFormState('INVALID'); - setPackageInfo(undefined); - } + const updatePackageInfo = useCallback( + (updatedPackageInfo: PackageInfo | undefined) => { + if (updatedPackageInfo) { + setPackageInfo(updatedPackageInfo); + if (agentConfig) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setPackageInfo(undefined); + } - // eslint-disable-next-line no-console - console.debug('Package info updated', updatedPackageInfo); - }; + // eslint-disable-next-line no-console + console.debug('Package info updated', updatedPackageInfo); + }, + [agentConfig, setPackageInfo, setFormState] + ); // Update agent config method - const updateAgentConfig = (updatedAgentConfig: AgentConfig | undefined) => { - if (updatedAgentConfig) { - setAgentConfig(updatedAgentConfig); - } else { - setFormState('INVALID'); - setAgentConfig(undefined); - } + const updateAgentConfig = useCallback( + (updatedAgentConfig: AgentConfig | undefined) => { + if (updatedAgentConfig) { + setAgentConfig(updatedAgentConfig); + if (packageInfo) { + setFormState('VALID'); + } + } else { + setFormState('INVALID'); + setAgentConfig(undefined); + } - // eslint-disable-next-line no-console - console.debug('Agent config updated', updatedAgentConfig); - }; + // eslint-disable-next-line no-console + console.debug('Agent config updated', updatedAgentConfig); + }, + [packageInfo, setAgentConfig, setFormState] + ); const hasErrors = validationResults ? validationHasErrors(validationResults) : false; @@ -156,18 +170,13 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } }; - // Cancel url - const CONFIG_URL = useLink( - `${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}` - ); - const PACKAGE_URL = useEPMLinks().toDetailView({ - name: (pkgkey || '-').split('-')[0], - version: (pkgkey || '-').split('-')[1], - }); - const cancelUrl = from === 'config' ? CONFIG_URL : PACKAGE_URL; + // Cancel path + const cancelUrl = + from === 'config' + ? getHref('configuration_details', { configId: agentConfig?.id || configId }) + : getHref('integration_details', { pkgkey }); // Save datasource - const [formState, setFormState] = useState('INVALID'); const saveDatasource = async () => { setFormState('LOADING'); const result = await sendCreateDatasource(datasource); @@ -186,7 +195,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${agentConfig ? agentConfig.id : configId}`); + history.push(getPath('configuration_details', { configId: agentConfig?.id || configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { defaultMessage: `Successfully added '{datasourceName}'`, @@ -219,33 +228,43 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { packageInfo, }; + const stepSelectConfig = useMemo( + () => ( + + ), + [pkgkey, updatePackageInfo, agentConfig, updateAgentConfig] + ); + + const stepSelectPackage = useMemo( + () => ( + + ), + [configId, updateAgentConfig, packageInfo, updatePackageInfo] + ); + const steps: EuiStepProps[] = [ from === 'package' ? { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectAgentConfigTitle', { defaultMessage: 'Select an agent configuration', }), - children: ( - - ), + children: stepSelectConfig, } : { title: i18n.translate('xpack.ingestManager.createDatasource.stepSelectPackageTitle', { defaultMessage: 'Select an integration', }), - children: ( - - ), + children: stepSelectPackage, }, { title: i18n.translate('xpack.ingestManager.createDatasource.stepDefineDatasourceTitle', { @@ -280,6 +299,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ) : null, }, ]; + return ( {formState === 'CONFIRM' && agentConfig && ( @@ -290,6 +310,16 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { onCancel={() => setFormState('VALID')} /> )} + {from === 'package' + ? packageInfo && ( + + ) + : agentConfig && ( + + )} {/* TODO #64541 - Remove classes */} @@ -331,3 +361,19 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ); }; + +const ConfigurationBreadcrumb: React.FunctionComponent<{ + configName: string; + configId: string; +}> = ({ configName, configId }) => { + useBreadcrumbs('add_datasource_from_configuration', { configName, configId }); + return null; +}; + +const IntegrationBreadcrumb: React.FunctionComponent<{ + pkgTitle: string; + pkgkey: string; +}> = ({ pkgTitle, pkgkey }) => { + useBreadcrumbs('add_datasource_from_integration', { pkgTitle, pkgkey }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx index a0418c5f256c4..3ad862c5e43fd 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/datasources/datasources_table.tsx @@ -20,7 +20,6 @@ import { AgentConfig, Datasource } from '../../../../../types'; import { TableRowActions } from '../../../components/table_row_actions'; import { DangerEuiContextMenuItem } from '../../../components/danger_eui_context_menu_item'; import { useCapabilities, useLink } from '../../../../../hooks'; -import { useAgentConfigLink } from '../../hooks/use_details_uri'; import { DatasourceDeleteProvider } from '../../../components/datasource_delete_provider'; import { useConfigRefresh } from '../../hooks/use_config'; import { PackageIcon } from '../../../../../components/package_icon'; @@ -54,9 +53,8 @@ export const DatasourcesTable: React.FunctionComponent = ({ config, ...rest }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); - const editDatasourceLink = useLink(`/configs/${config.id}/edit-datasource`); const refreshConfig = useConfigRefresh(); // With the datasources provided on input, generate the list of datasources @@ -216,7 +214,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ = ({ ], }, ], - [config, editDatasourceLink, hasWriteCapabilities, refreshConfig] + [config, getHref, hasWriteCapabilities, refreshConfig] ); return ( @@ -274,9 +275,10 @@ export const DatasourcesTable: React.FunctionComponent = ({ search={{ toolsRight: [ (({ configId }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId }); return ( (({ configId }) => { /> } actions={ - + ( fleet: { enabled: isFleetEnabled }, } = useConfig(); const history = useHistory(); + const { getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const refreshConfig = useConfigRefresh(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -147,7 +148,7 @@ export const ConfigSettingsView = memo<{ config: AgentConfig }>( validation={validation} isEditing={true} onDelete={() => { - history.push(AGENT_CONFIG_PATH); + history.push(getPath('configurations_list')); }} /> {/* TODO #64541 - Remove classes */} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts deleted file mode 100644 index 787791f985c7d..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; - -export const DETAILS_ROUTER_PATH = `${AGENT_CONFIG_DETAILS_PATH}:configId`; -export const DETAILS_ROUTER_SUB_PATH = `${DETAILS_ROUTER_PATH}/:tabId`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts deleted file mode 100644 index 9332ce3e0f909..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/hooks/use_details_uri.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { generatePath } from 'react-router-dom'; -import { useLink } from '../../../../hooks'; -import { AGENT_CONFIG_PATH } from '../../../../constants'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from '../constants'; - -type AgentConfigUriArgs = - | ['list'] - | ['details', { configId: string }] - | ['details-yaml', { configId: string }] - | ['details-settings', { configId: string }] - | ['datasource', { configId: string; datasourceId: string }] - | ['add-datasource', { configId: string }]; - -/** - * Returns a Uri that starts at the Agent Config Route path (`/configs/`). - * These are good for use when needing to use React Router's redirect or - * `history.push(routePath)`. - * @param args - */ -export const useAgentConfigUri = (...args: AgentConfigUriArgs) => { - switch (args[0]) { - case 'list': - return AGENT_CONFIG_PATH; - case 'details': - return generatePath(DETAILS_ROUTER_PATH, args[1]); - case 'details-yaml': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'yaml' })}`; - case 'details-settings': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'settings' })}`; - case 'add-datasource': - return `${generatePath(DETAILS_ROUTER_SUB_PATH, { ...args[1], tabId: 'add-datasource' })}`; - case 'datasource': - const [, options] = args; - return `${generatePath(DETAILS_ROUTER_PATH, options)}?datasourceId=${options.datasourceId}`; - } - return '/'; -}; - -/** - * Returns a full Link that includes Kibana basepath (ex. `/app/ingestManager#/configs`). - * These are good for use in `href` properties - * @param args - */ -export const useAgentConfigLink = (...args: AgentConfigUriArgs) => { - const BASE_URI = useLink(''); - const AGENT_CONFIG_ROUTE = useAgentConfigUri(...args); - return `${BASE_URI}${AGENT_CONFIG_ROUTE}`; -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx index 82879c174b7d3..f80b981b69d3b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; @@ -21,13 +21,13 @@ import { } from '@elastic/eui'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import styled from 'styled-components'; -import { useGetOneAgentConfig } from '../../../hooks'; +import { AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; +import { useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { Loading } from '../../../components'; import { WithHeaderLayout } from '../../../layouts'; import { ConfigRefreshContext, useGetAgentStatus, AgentStatusRefreshContext } from './hooks'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from './hooks/use_details_uri'; -import { DETAILS_ROUTER_PATH, DETAILS_ROUTER_SUB_PATH } from './constants'; import { ConfigDatasourcesView } from './components/datasources'; import { ConfigYamlView } from './components/yaml'; import { ConfigSettingsView } from './components/settings'; @@ -38,23 +38,11 @@ const Divider = styled.div` border-left: ${props => props.theme.eui.euiBorderThin}; `; -export const AgentConfigDetailsPage = memo(() => { - return ( - - - - - - - - - ); -}); - -export const AgentConfigDetailsLayout: React.FunctionComponent = () => { +export const AgentConfigDetailsPage: React.FunctionComponent = () => { const { params: { configId, tabId = '' }, } = useRouteMatch<{ configId: string; tabId?: string }>(); + const { getHref } = useLink(); const agentConfigRequest = useGetOneAgentConfig(configId); const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; const { isLoading, error, sendRequest: refreshAgentConfig } = agentConfigRequest; @@ -63,17 +51,16 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { const { refreshAgentStatus } = agentStatusRequest; const agentStatus = agentStatusRequest.data?.results; - // Links - const configListLink = useAgentConfigLink('list'); - const configDetailsLink = useAgentConfigLink('details', { configId }); - const configDetailsYamlLink = useAgentConfigLink('details-yaml', { configId }); - const configDetailsSettingsLink = useAgentConfigLink('details-settings', { configId }); - const headerLeftContent = useMemo( () => ( - + { ) : null} ), - [configListLink, agentConfig, configId] + [getHref, agentConfig, configId] ); const headerRightContent = useMemo( @@ -184,7 +171,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.datasourcesTabText', { defaultMessage: 'Data sources', }), - href: configDetailsLink, + href: getHref('configuration_details', { configId, tabId: 'datasources' }), isSelected: tabId === '' || tabId === 'datasources', }, { @@ -192,7 +179,7 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.yamlTabText', { defaultMessage: 'YAML', }), - href: configDetailsYamlLink, + href: getHref('configuration_details', { configId, tabId: 'yaml' }), isSelected: tabId === 'yaml', }, { @@ -200,11 +187,11 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.configDetails.subTabs.settingsTabText', { defaultMessage: 'Settings', }), - href: configDetailsSettingsLink, + href: getHref('configuration_details', { configId, tabId: 'settings' }), isSelected: tabId === 'settings', }, ]; - }, [configDetailsLink, configDetailsSettingsLink, configDetailsYamlLink, tabId]); + }, [getHref, configId, tabId]); if (redirectToAgentConfigList) { return ; @@ -254,28 +241,37 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => { rightColumn={headerRightContent} tabs={(headerTabs as unknown) as EuiTabProps[]} > - - { - return ; - }} - /> - { - return ; - }} - /> - { - return ; - }} - /> - + ); }; + +const AgentConfigDetailsContent: React.FunctionComponent<{ agentConfig: AgentConfig }> = ({ + agentConfig, +}) => { + useBreadcrumbs('configuration_details', { configName: agentConfig.name }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx index 089a5a91df88a..92be20a2761e2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/edit_datasource_page/index.tsx @@ -16,10 +16,10 @@ import { EuiFlexItem, EuiSpacer, } from '@elastic/eui'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; import { useLink, + useBreadcrumbs, useCore, useConfig, sendUpdateDatasource, @@ -53,6 +53,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { params: { configId, datasourceId }, } = useRouteMatch(); const history = useHistory(); + const { getHref, getPath } = useLink(); const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); useEffect(() => { @@ -185,8 +186,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { }; // Cancel url - const CONFIG_URL = useLink(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); - const cancelUrl = CONFIG_URL; + const cancelUrl = getHref('configuration_details', { configId }); // Save datasource const [formState, setFormState] = useState('INVALID'); @@ -208,7 +208,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { } const { error } = await saveDatasource(); if (!error) { - history.push(`${AGENT_CONFIG_DETAILS_PATH}${configId}`); + history.push(getPath('configuration_details', { configId })); notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.editDatasource.updatedNotificationTitle', { defaultMessage: `Successfully updated '{datasourceName}'`, @@ -262,6 +262,7 @@ export const EditDatasourcePage: React.FunctionComponent = () => { /> ) : ( <> + {formState === 'CONFIRM' && ( { ); }; + +const Breadcrumb: React.FunctionComponent<{ configName: string; configId: string }> = ({ + configName, + configId, +}) => { + useBreadcrumbs('edit_datasource', { configName, configId }); + return null; +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx index ef88aa5d17f1e..74fa67078f741 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/index.tsx @@ -5,26 +5,32 @@ */ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useBreadcrumbs } from '../../hooks'; import { AgentConfigListPage } from './list_page'; import { AgentConfigDetailsPage } from './details_page'; import { CreateDatasourcePage } from './create_datasource_page'; import { EditDatasourcePage } from './edit_datasource_page'; -export const AgentConfigApp: React.FunctionComponent = () => ( - - - - - - - - - - - - - - - - -); +export const AgentConfigApp: React.FunctionComponent = () => { + useBreadcrumbs('configurations'); + + return ( + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 9b565a0452c96..ff3124d574857 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -22,11 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; import { AgentConfig } from '../../../types'; -import { - AGENT_CONFIG_DETAILS_PATH, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - AGENT_CONFIG_PATH, -} from '../../../constants'; +import { AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../constants'; import { WithHeaderLayout } from '../../../layouts'; import { useCapabilities, @@ -35,11 +31,11 @@ import { useLink, useConfig, useUrlParams, + useBreadcrumbs, } from '../../../hooks'; import { CreateAgentConfigFlyout } from './components'; import { SearchBar } from '../../../components/search_bar'; import { LinkedAgentCount } from '../components'; -import { useAgentConfigLink } from '../details_page/hooks/use_details_uri'; import { TableRowActions } from '../components/table_row_actions'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ @@ -81,14 +77,17 @@ const AgentConfigListPageLayout: React.FunctionComponent = ({ children }) => ( const ConfigRowActions = memo<{ config: AgentConfig; onDelete: () => void }>( ({ config, onDelete }) => { + const { getHref } = useLink(); const hasWriteCapabilities = useCapabilities().write; - const detailsLink = useAgentConfigLink('details', { configId: config.id }); - const addDatasourceLink = useAgentConfigLink('add-datasource', { configId: config.id }); return ( + void }>( void }>( ); export const AgentConfigListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('configurations_list'); + const { getHref, getPath } = useLink(); // Config information const hasWriteCapabilites = useCapabilities().write; const { fleet: { enabled: isFleetEnabled }, } = useConfig(); - // Base URL paths - const DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - // Table and search states const { urlParams, toUrlParams } = useUrlParams(); const [search, setSearch] = useState( @@ -142,14 +140,16 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { (isOpen: boolean) => { if (isOpen !== isCreateAgentConfigFlyoutOpen) { if (isOpen) { - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams({ ...urlParams, create: null })}`); + history.push( + `${getPath('configurations_list')}?${toUrlParams({ ...urlParams, create: null })}` + ); } else { const { create, ...params } = urlParams; - history.push(`${AGENT_CONFIG_PATH}?${toUrlParams(params)}`); + history.push(`${getPath('configurations_list')}?${toUrlParams(params)}`); } } }, - [history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] + [getPath, history, isCreateAgentConfigFlyoutOpen, toUrlParams, urlParams] ); // Fetch agent configs @@ -174,7 +174,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { @@ -253,7 +253,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { } return cols; - }, [DETAILS_URI, isFleetEnabled, sendRequest]); + }, [getHref, isFleetEnabled, sendRequest]); const createAgentConfigButton = useMemo( () => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx index 7b0641e66fd43..0fdba54a04145 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -5,13 +5,14 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { DataStreamListPage } from './list_page'; export const DataStreamApp: React.FunctionComponent = () => { return ( - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index cff138c6a16ca..09873a3cdaa87 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; -import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; +import { useGetDataStreams, useStartDeps, usePagination, useBreadcrumbs } from '../../../hooks'; import { PackageIcon } from '../../../components/package_icon'; import { DataStreamRowActions } from './components/data_stream_row_actions'; @@ -55,6 +55,8 @@ const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( ); export const DataStreamListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('data_streams'); + const { data: { fieldFormats }, } = useStartDeps(); @@ -239,7 +241,12 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { sorting={true} search={{ toolsRight: [ - sendRequest()}> + sendRequest()} + > } - href={url} + href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })} /> ); } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx index d4ed3624a6e68..436163bafcfe4 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_links.tsx @@ -3,32 +3,15 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { generatePath } from 'react-router-dom'; import { useCore } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; -import { DetailViewPanelName } from '../../../types'; -import { BASE_PATH, EPM_PATH, EPM_DETAIL_VIEW_PATH } from '../../../constants'; - -// TODO: get this from server/packages/handlers.ts (move elsewhere?) -// seems like part of the name@version change -interface DetailParams { - name: string; - version: string; - panel?: DetailViewPanelName; - withAppRoot?: boolean; -} const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; export function useLinks() { const { http } = useCore(); - function appRoot(path: string) { - // include '#' because we're using HashRouter - return http.basePath.prepend(BASE_PATH + '#' + path); - } - return { toAssets: (path: string) => http.basePath.prepend( @@ -49,13 +32,5 @@ export function useLinks() { const filePath = `${epmRouteService.getInfoPath(pkgkey)}/${imagePath}`; return http.basePath.prepend(filePath); }, - toListView: () => appRoot(EPM_PATH), - toDetailView: ({ name, version, panel, withAppRoot = true }: DetailParams) => { - // panel is optional, but `generatePath` won't accept `path: undefined` - // so use this to pass `{ pkgkey }` or `{ pkgkey, panel }` - const params = Object.assign({ pkgkey: `${name}-${version}` }, panel ? { panel } : {}); - const path = generatePath(EPM_DETAIL_VIEW_PATH, params); - return withAppRoot ? appRoot(path) : path; - }, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx index 244a9a2c7426e..36b81e786b935 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/hooks/use_package_install.tsx @@ -6,12 +6,12 @@ import createContainer from 'constate'; import React, { useCallback, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { NotificationsStart } from 'src/core/public'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { PackageInfo } from '../../../types'; -import { sendInstallPackage, sendRemovePackage } from '../../../hooks'; -import { useLinks } from '.'; +import { sendInstallPackage, sendRemovePackage, useLink } from '../../../hooks'; import { InstallStatus } from '../../../types'; interface PackagesInstall { @@ -29,7 +29,8 @@ type InstallPackageProps = Pick & { type SetPackageInstallStatusProps = Pick & PackageInstallItem; function usePackageInstall({ notifications }: { notifications: NotificationsStart }) { - const { toDetailView } = useLinks(); + const history = useHistory(); + const { getPath } = useLink(); const [packages, setPackage] = useState({}); const setPackageInstallStatus = useCallback( @@ -88,12 +89,11 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar } else { setPackageInstallStatus({ name, status: InstallStatus.installed, version }); if (fromUpdate) { - const settingsUrl = toDetailView({ - name, - version, + const settingsPath = getPath('integration_details', { + pkgkey: `${name}-${version}`, panel: 'settings', }); - window.location.href = settingsUrl; + history.push(settingsPath); } notifications.toasts.addSuccess({ title: toMountPoint( @@ -113,7 +113,7 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar }); } }, - [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, toDetailView] + [getPackageInstallStatus, notifications.toasts, setPackageInstallStatus, getPath, history] ); const uninstallPackage = useCallback( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx index 2c8ee7ca2fcf3..ca1a8df534044 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/index.tsx @@ -6,24 +6,26 @@ import React from 'react'; import { HashRouter as Router, Switch, Route } from 'react-router-dom'; -import { useConfig } from '../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../constants'; +import { useConfig, useBreadcrumbs } from '../../hooks'; import { CreateDatasourcePage } from '../agent_config/create_datasource_page'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; export const EPMApp: React.FunctionComponent = () => { + useBreadcrumbs('integrations'); const { epm } = useConfig(); return epm.enabled ? ( - + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx index c82b7ed2297a7..7459c943fa831 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/data_sources_panel.tsx @@ -7,29 +7,22 @@ import React, { Fragment } from 'react'; import { EuiTitle } from '@elastic/eui'; import { Redirect } from 'react-router-dom'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus } from '../../../../types'; +import { useLink } from '../../../../hooks'; interface DataSourcesPanelProps { name: string; version: string; } export const DataSourcesPanel = ({ name, version }: DataSourcesPanelProps) => { - const { toDetailView } = useLinks(); + const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab if (packageInstallStatus.status !== InstallStatus.installed) - return ( - - ); + return ; return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx index cf51296d468a9..5c2d1373d0b0e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx @@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui'; import { PackageInfo } from '../../../../types'; -import { EPM_PATH } from '../../../../constants'; import { useCapabilities, useLink } from '../../../../hooks'; import { IconPanel } from '../../components/icon_panel'; import { NavButtonBack } from '../../components/nav_button_back'; -import { useLinks } from '../../hooks'; import { CenterColumn, LeftColumn, RightColumn } from './layout'; import { UpdateIcon } from '../../components/icons'; @@ -36,14 +34,13 @@ export function Header(props: HeaderProps) { installedVersion = props.savedObject.attributes.version; } const hasWriteCapabilites = useCapabilities().write; - const { toListView } = useLinks(); - const ADD_DATASOURCE_URI = useLink(`${EPM_PATH}/${name}-${version}/add-datasource`); + const { getHref } = useLink(); const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false; return ( & Pick; export function DetailLayout(props: LayoutProps) { - const { name: packageName, version, icons, restrictWidth } = props; + const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props; const iconType = usePackageIconType({ packageName, version, icons }); + useBreadcrumbs('integration_details', { pkgTitle: packageTitle }); return ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx index aa63cf2ba175d..65a437269ec6a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/side_nav_links.tsx @@ -8,7 +8,8 @@ import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiButtonEmptyProps } from '@elastic/eui'; import { PackageInfo, entries, DetailViewPanelName, InstallStatus } from '../../../../types'; -import { useLinks, useGetPackageInstallStatus } from '../../hooks'; +import { useLink } from '../../../../hooks'; +import { useGetPackageInstallStatus } from '../../hooks'; export type NavLinkProps = Pick & { active: DetailViewPanelName; @@ -27,7 +28,7 @@ const PanelDisplayNames: Record = { }; export function SideNavLinks({ name, version, active }: NavLinkProps) { - const { toDetailView } = useLinks(); + const { getHref } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); @@ -35,7 +36,7 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) { {entries(PanelDisplayNames).map(([panel, display]) => { const Link = styled(EuiButtonEmpty).attrs({ - href: toDetailView({ name, version, panel }), + href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }), })` font-weight: ${p => active === panel diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx index 983a322de1088..84ad3593a5bf1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx @@ -8,11 +8,8 @@ import React, { useState } from 'react'; import { useRouteMatch, Switch, Route } from 'react-router-dom'; import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; -import { - EPM_LIST_ALL_PACKAGES_PATH, - EPM_LIST_INSTALLED_PACKAGES_PATH, -} from '../../../../constants'; -import { useLink, useGetCategories, useGetPackages } from '../../../../hooks'; +import { PAGE_ROUTING_PATHS } from '../../../../constants'; +import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks'; import { WithHeaderLayout } from '../../../../layouts'; import { CategorySummaryItem } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; @@ -23,9 +20,7 @@ export function EPMHomePage() { const { params: { tabId }, } = useRouteMatch<{ tabId?: string }>(); - - const ALL_PACKAGES_URI = useLink(EPM_LIST_ALL_PACKAGES_PATH); - const INSTALLED_PACKAGES_URI = useLink(EPM_LIST_INSTALLED_PACKAGES_PATH); + const { getHref } = useLink(); return ( - + - + @@ -65,6 +60,7 @@ export function EPMHomePage() { } function InstalledPackages() { + useBreadcrumbs('integrations_installed'); const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages(); const [selectedCategory, setSelectedCategory] = useState(''); @@ -117,6 +113,7 @@ function InstalledPackages() { } function AvailablePackages() { + useBreadcrumbs('integrations_all'); const [selectedCategory, setSelectedCategory] = useState(''); const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({ category: selectedCategory, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx index 6a1e6dc226903..03f1a67fe95ab 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/agent_details.tsx @@ -14,7 +14,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Agent, AgentConfig } from '../../../../types'; -import { AGENT_CONFIG_DETAILS_PATH } from '../../../../constants'; import { useLink } from '../../../../hooks'; import { AgentHealth } from '../../components'; @@ -22,7 +21,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ agent: Agent; agentConfig?: AgentConfig; }> = memo(({ agent, agentConfig }) => { - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); + const { getHref } = useLink(); return ( {[ @@ -53,7 +52,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ defaultMessage: 'Agent configuration', }), description: agentConfig ? ( - + {agentConfig.name || agent.config_id} ) : ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index aa46f7cf976cd..2ebc495d5dda7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -19,16 +19,13 @@ import { import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { AgentRefreshContext } from './hooks'; -import { - FLEET_AGENTS_PATH, - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, -} from '../../../constants'; +import { Agent, AgentConfig } from '../../../types'; +import { PAGE_ROUTING_PATHS } from '../../../constants'; import { Loading, Error } from '../../../components'; -import { useGetOneAgent, useGetOneAgentConfig, useLink } from '../../../hooks'; +import { useGetOneAgent, useGetOneAgentConfig, useLink, useBreadcrumbs } from '../../../hooks'; import { WithHeaderLayout } from '../../../layouts'; import { AgentHealth } from '../components'; +import { AgentRefreshContext } from './hooks'; import { AgentEventsTable, AgentDetailsActionMenu, AgentDetailsContent } from './components'; const Divider = styled.div` @@ -41,6 +38,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { const { params: { agentId, tabId = '' }, } = useRouteMatch<{ agentId: string; tabId?: string }>(); + const { getHref } = useLink(); const { isLoading, isInitialRequest, @@ -56,16 +54,16 @@ export const AgentDetailsPage: React.FunctionComponent = () => { sendRequest: sendAgentConfigRequest, } = useGetOneAgentConfig(agentData?.item?.config_id); - const agentListUrl = useLink(FLEET_AGENTS_PATH); - const agentActivityTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/activity`); - const agentDetailsTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/details`); - const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH); - const headerLeftContent = useMemo( () => ( - + { ), - [agentData, agentId, agentListUrl] + [agentData, agentId, getHref] ); const headerRightContent = useMemo( @@ -114,7 +112,9 @@ export const AgentDetailsPage: React.FunctionComponent = () => { content: isAgentConfigLoading ? ( ) : agentConfigData?.item ? ( - + {agentConfigData.item.name || agentData.item.config_id} ) : ( @@ -143,7 +143,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ) : ( undefined ), - [agentConfigData, agentConfigUrl, agentData, isAgentConfigLoading] + [agentConfigData, agentData, getHref, isAgentConfigLoading] ); const headerTabs = useMemo(() => { @@ -153,7 +153,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.activityLogTab', { defaultMessage: 'Activity log', }), - href: agentActivityTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'activity' }), isSelected: !tabId || tabId === 'activity', }, { @@ -161,11 +161,11 @@ export const AgentDetailsPage: React.FunctionComponent = () => { name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.detailsTab', { defaultMessage: 'Agent details', }), - href: agentDetailsTabUrl, + href: getHref('fleet_agent_details', { agentId, tabId: 'details' }), isSelected: tabId === 'details', }, ]; - }, [agentActivityTabUrl, agentDetailsTabUrl, tabId]); + }, [getHref, agentId, tabId]); return ( { error={error} /> ) : agentData && agentData.item ? ( - - { - return ( - - ); - }} - /> - { - return ; - }} - /> - + ) : ( { error={i18n.translate( 'xpack.ingestManager.agentDetails.agentNotFoundErrorDescription', { - defaultMessage: 'Cannot found agent ID {agentId}', + defaultMessage: 'Cannot find agent ID {agentId}', values: { agentId, }, @@ -233,3 +218,32 @@ export const AgentDetailsPage: React.FunctionComponent = () => { ); }; + +const AgentDetailsPageContent: React.FunctionComponent<{ + agent: Agent; + agentConfig?: AgentConfig; +}> = ({ agent, agentConfig }) => { + useBreadcrumbs('fleet_agent_details', { + agentHost: + typeof agent.local_metadata.host === 'object' && + typeof agent.local_metadata.host.hostname === 'string' + ? agent.local_metadata.host.hostname + : '-', + }); + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 84056df2aca32..56cc0028f0cf9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -34,17 +34,14 @@ import { useGetAgents, useUrlParams, useLink, + useBreadcrumbs, } from '../../../hooks'; -import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; +import { AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; import { AgentStatusKueryHelper } from '../../../services'; -import { - FLEET_AGENT_DETAIL_PATH, - AGENT_CONFIG_DETAILS_PATH, - AGENT_SAVED_OBJECT_TYPE, -} from '../../../constants'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -77,8 +74,8 @@ const statusFilters = [ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( ({ agent, refresh, onReassignClick }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); const [isOpen, setIsOpen] = useState(false); const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); @@ -101,7 +98,11 @@ const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refre > + = () => { + useBreadcrumbs('fleet_agent_list'); + const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasWriteCapabilites = useCapabilities().write; + // Agent data states const [showInactive, setShowInactive] = useState(false); @@ -241,8 +245,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; const { isLoading: isAgentConfigsLoading } = agentConfigsRequest; - const CONFIG_DETAILS_URI = useLink(AGENT_CONFIG_DETAILS_PATH); - const columns = [ { field: 'local_metadata.host.hostname', @@ -250,9 +252,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Host', }), render: (host: string, agent: Agent) => ( - + {safeMetadata(host)} - + ), }, { @@ -274,7 +276,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx index ff7c2f705e7b7..43173124d6bae 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/index.tsx @@ -23,10 +23,14 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentConfig } from '../../../../types'; import { EnrollmentStepAgentConfig } from './config_selection'; -import { useGetOneEnrollmentAPIKey, useCore, useGetSettings, useLink } from '../../../../hooks'; +import { + useGetOneEnrollmentAPIKey, + useCore, + useGetSettings, + useLink, + useFleetStatus, +} from '../../../../hooks'; import { ManualInstructions } from '../../../../components/enrollment_instructions'; -import { FLEET_PATH } from '../../../../constants'; -import { useFleetStatus } from '../../../../hooks/use_fleet_status'; interface Props { onClose: () => void; @@ -37,9 +41,9 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ onClose, agentConfigs = [], }) => { + const { getHref } = useLink(); const core = useCore(); const fleetStatus = useFleetStatus(); - const fleetLink = useLink(FLEET_PATH); const [selectedAPIKeyId, setSelectedAPIKeyId] = useState(); @@ -120,7 +124,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent = ({ defaultMessage="Fleet needs to be set up before agents can be enrolled. {link}" values={{ link: ( - + = ({ children }) => { + const { getHref } = useLink(); const hasWriteCapabilites = useCapabilities().write; const agentStatusRequest = useGetAgentStatus(undefined, { pollIntervalMs: REFRESH_INTERVAL_MS, @@ -163,8 +164,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Agents" /> ), - isSelected: routeMatch.path === FLEET_AGENTS_PATH, - href: useLink(FLEET_AGENTS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_agent_list, + href: getHref('fleet_agent_list'), }, { name: ( @@ -173,8 +174,8 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => { defaultMessage="Enrollment tokens" /> ), - isSelected: routeMatch.path === FLEET_ENROLLMENT_TOKENS_PATH, - href: useLink(FLEET_ENROLLMENT_TOKENS_PATH), + isSelected: routeMatch.path === PAGE_ROUTING_PATHS.fleet_enrollment_tokens, + href: getHref('fleet_enrollment_tokens'), }, ] as unknown) as EuiTabProps[] } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx deleted file mode 100644 index 8af0e0a5cbc25..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/child_routes.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -interface RouteConfig { - path: string; - component: React.ComponentType; - routes?: RouteConfig[]; -} - -export const ChildRoutes: React.FunctionComponent<{ - routes?: RouteConfig[]; - useSwitch?: boolean; - [other: string]: any; -}> = ({ routes, useSwitch = true, ...rest }) => { - if (!routes) { - return null; - } - const Parent = useSwitch ? Switch : React.Fragment; - return ( - - {routes.map(route => ( - { - const Component = route.component; - return ; - }} - /> - ))} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx deleted file mode 100644 index 489ee85ffe28a..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/navigation/connected_link.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link, withRouter } from 'react-router-dom'; - -export function ConnectedLinkComponent({ - location, - path, - query, - disabled, - children, - ...props -}: { - location: any; - path: string; - disabled: boolean; - query: any; - [key: string]: any; -}) { - if (disabled) { - return ; - } - - // Shorthand for pathname - const pathname = path || _.get(props.to, 'pathname') || location.pathname; - - return ( - - ); -} - -export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index c11e3a49c7693..add495ce0c194 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -20,6 +20,7 @@ import { import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../../constants'; import { + useBreadcrumbs, usePagination, useGetEnrollmentAPIKeys, useGetAgentConfigs, @@ -125,6 +126,7 @@ const DeleteButton: React.FunctionComponent<{ apiKey: EnrollmentAPIKey; refresh: }; export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { + useBreadcrumbs('fleet_enrollment_tokens'); const [flyoutOpen, setFlyoutOpen] = useState(false); const [search, setSearch] = useState(''); const { pagination, setPagination, pageSizeOptions } = usePagination(); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx index c820a9b867b63..9bb77ca44b848 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/index.tsx @@ -5,17 +5,18 @@ */ import React from 'react'; import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'; +import { PAGE_ROUTING_PATHS } from '../../constants'; import { Loading } from '../../components'; -import { useConfig, useCore } from '../../hooks'; +import { useConfig, useCore, useFleetStatus, useBreadcrumbs } from '../../hooks'; import { AgentListPage } from './agent_list_page'; import { SetupPage } from './setup_page'; import { AgentDetailsPage } from './agent_details_page'; import { NoAccessPage } from './error_pages/no_access'; import { EnrollmentTokenListPage } from './enrollment_token_list_page'; import { ListLayout } from './components/list_layout'; -import { useFleetStatus } from '../../hooks/use_fleet_status'; export const FleetApp: React.FunctionComponent = () => { + useBreadcrumbs('fleet'); const core = useCore(); const { fleet } = useConfig(); @@ -41,16 +42,20 @@ export const FleetApp: React.FunctionComponent = () => { return ( - } /> - + } + /> + - + - + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx index 0f6d3c5b55ce6..6e61a55466e87 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/overview/components/agent_section.tsx @@ -16,10 +16,10 @@ import { import { OverviewPanel } from './overview_panel'; import { OverviewStats } from './overview_stats'; import { useLink, useGetAgentStatus } from '../../../hooks'; -import { FLEET_PATH } from '../../../constants'; import { Loading } from '../../fleet/components'; export const OverviewAgentSection = () => { + const { getHref } = useLink(); const agentStatusRequest = useGetAgentStatus({}); return ( @@ -34,7 +34,7 @@ export const OverviewAgentSection = () => { /> - + = ({ agentConfigs, }) => { + const { getHref } = useLink(); const datasourcesRequest = useGetDatasources({ page: 1, perPage: 10000, @@ -40,7 +40,7 @@ export const OverviewConfigurationSection: React.FC<{ agentConfigs: AgentConfig[ /> - + { + const { getHref } = useLink(); const datastreamRequest = useGetDataStreams(); const { data: { fieldFormats }, @@ -55,7 +55,7 @@ export const OverviewDatastreamSection: React.FC = () => { /> - + { + const { getHref } = useLink(); const packagesRequest = useGetPackages(); const res = packagesRequest.data?.response; const total = res?.length ?? 0; @@ -40,7 +40,7 @@ export const OverviewIntegrationSection: React.FC = () => { /> - + { + useBreadcrumbs('overview'); + // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 2c6ed9d81744e..fd4e08f619495 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -64,8 +64,13 @@ export class IngestManagerPlugin IngestManagerStartDeps, IngestManagerStart ]; - const { renderApp } = await import('./applications/ingest_manager'); - return renderApp(coreStart, params, deps, startDeps, config); + const { renderApp, teardownIngestManager } = await import('./applications/ingest_manager'); + const unmount = renderApp(coreStart, params, deps, startDeps, config); + + return () => { + unmount(); + teardownIngestManager(coreStart); + }; }, }); } diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 80a33c26d86da..666d46f030780 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -136,12 +136,13 @@ export const getListHandler: RequestHandler = async (context, request, response) dashboards: enhancedDashboards, }; } + return { index: indexName, dataset: datasetBuckets.length ? datasetBuckets[0].key : '', namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', type: typeBuckets.length ? typeBuckets[0].key : '', - package: pkg, + package: pkgSavedObject.length ? pkg : '', package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', last_activity: lastActivity, size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, diff --git a/x-pack/plugins/lens/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/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 ( { await PageObjects.common.sleep(5000); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { 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 () => {