diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index efba93350b8fb..2a5fc914662b6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) -- [ ] If a plugin configuration key changed, check if it needs to be whitelisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) +- [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md new file mode 100644 index 0000000000000..0fdf36bc719ec --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) + +## ExpressionsInspectorAdapter.ast property + +Signature: + +```typescript +get ast(): any; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md new file mode 100644 index 0000000000000..671270a5c78ce --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) > [logAST](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) + +## ExpressionsInspectorAdapter.logAST() method + +Signature: + +```typescript +logAST(ast: any): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| ast | any | | + +Returns: + +`void` + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md new file mode 100644 index 0000000000000..23d542a0f69eb --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) + +## ExpressionsInspectorAdapter class + +Signature: + +```typescript +export declare class ExpressionsInspectorAdapter extends EventEmitter +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [ast](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.ast.md) | | any | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [logAST(ast)](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.logast.md) | | | + diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md index 1b97c9e11f83c..e3eb7a34175ee 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.md @@ -16,6 +16,7 @@ | [ExpressionRenderer](./kibana-plugin-plugins-expressions-public.expressionrenderer.md) | | | [ExpressionRendererRegistry](./kibana-plugin-plugins-expressions-public.expressionrendererregistry.md) | | | [ExpressionRenderHandler](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.md) | | +| [ExpressionsInspectorAdapter](./kibana-plugin-plugins-expressions-public.expressionsinspectoradapter.md) | | | [ExpressionsPublicPlugin](./kibana-plugin-plugins-expressions-public.expressionspublicplugin.md) | | | [ExpressionsService](./kibana-plugin-plugins-expressions-public.expressionsservice.md) | ExpressionsService class is used for multiple purposes:1. It implements the same Expressions service that can be used on both: (1) server-side and (2) browser-side. 2. It implements the same Expressions service that users can fork/clone, thus have their own instance of the Expressions plugin. 3. ExpressionsService defines the public contracts of \*setup\* and \*start\* Kibana Platform life-cycles for ease-of-use on server-side and browser-side. 4. ExpressionsService creates a bound version of all exported contract functions. 5. Functions are bound the way there are:\`\`\`ts registerFunction = (...args: Parameters<Executor\['registerFunction'\]> ): ReturnType<Executor\['registerFunction'\]> => this.executor.registerFunction(...args); \`\`\`so that JSDoc appears in developers IDE when they use those plugins.expressions.registerFunction(. | | [ExpressionType](./kibana-plugin-plugins-expressions-public.expressiontype.md) | | diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 7e7c8953fd527..c2306b80734d8 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -458,7 +458,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation. +Enables legacy charts library for area, line and bar charts in visualize. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** diff --git a/docs/management/managing-tags.asciidoc b/docs/management/managing-tags.asciidoc index 3da98b2281fdc..88fdef66a7418 100644 --- a/docs/management/managing-tags.asciidoc +++ b/docs/management/managing-tags.asciidoc @@ -37,8 +37,7 @@ Create a tag to assign to your saved objects. image::images/tags/create-tag.png[Tag creation popin] . Enter a name and select a color for the new tag. + -The name can include alphanumeric characters (English letters and digits), `:`, `-`, `_` and the space character, -and cannot be longer than 50 characters. +The name cannot be longer than 50 characters. . Click *Create tag*. [float] diff --git a/docs/setup/upgrade/upgrade-migrations.asciidoc b/docs/setup/upgrade/upgrade-migrations.asciidoc index 74d097164c4a7..7436536d22781 100644 --- a/docs/setup/upgrade/upgrade-migrations.asciidoc +++ b/docs/setup/upgrade/upgrade-migrations.asciidoc @@ -1,11 +1,13 @@ [[upgrade-migrations]] === Upgrade migrations -Every time {kib} is upgraded it checks to see if all saved objects, such as dashboards, visualizations, and index patterns, are compatible with the new version. If any saved objects need to be updated, then the automatic saved object migration process is kicked off. +Every time {kib} is upgraded it will perform an upgrade migration to ensure that all <> are compatible with the new version. NOTE: 6.7 includes an https://www.elastic.co/guide/en/kibana/6.7/upgrade-assistant.html[Upgrade Assistant] to help you prepare for your upgrade to 7.0. To access the assistant, go to *Management > 7.0 Upgrade Assistant*. +WARNING: {kib} 7.12.0 and later uses a new migration process and index naming scheme. Be sure to read the documentation for your version of {kib} before proceeding. + WARNING: The following instructions assumes {kib} is using the default index names. If the `kibana.index` or `xpack.tasks.index` configuration settings were changed these instructions will have to be adapted accordingly. [float] @@ -14,19 +16,35 @@ WARNING: The following instructions assumes {kib} is using the default index nam Saved objects are stored in two indices: -* `.kibana_N`, or if set, the `kibana.index` configuration setting -* `.kibana_task_manager_N`, or if set, the `xpack.tasks.index` configuration setting +* `.kibana_{kibana_version}_001`, or if the `kibana.index` configuration setting is set `.{kibana.index}_{kibana_version}_001`. E.g. for Kibana v7.12.0 `.kibana_7.12.0_001`. +* `.kibana_task_manager_{kibana_version}_001`, or if the `xpack.tasks.index` configuration setting is set `.{xpack.tasks.index}_{kibana_version}_001` E.g. for Kibana v7.12.0 `.kibana_task_manager_7.12.0_001`. -For each of these indices, `N` is a number that increments every time {kib} runs an upgrade migration on that index. The index aliases `.kibana` and `.kibana_task_manager` point to the most up-to-date index. +The index aliases `.kibana` and `.kibana_task_manager` will always point to the most up-to-date version indices. + +The first time a newer {kib} starts, it will first perform an upgrade migration before starting plugins or serving HTTP traffic. To prevent losing acknowledged writes old nodes should be shutdown before starting the upgrade. To reduce the likelihood of old nodes losing acknowledged writes, {kib} 7.12.0 and later will add a write block to the outdated index. Table 1 lists the saved objects indices used by previous versions of {kib}. + +.Saved object indices and aliases per {kib} version +[options="header"] +[cols="a,a,a"] +|======================= +|Upgrading from version | Outdated index (alias) | Upgraded index (alias) +| 6.0.0 through 6.4.x | `.kibana` 1.3+^.^| `.kibana_7.12.0_001` +(`.kibana` alias) + +`.kibana_task_manager_7.12.0_001` (`.kibana_task_manager` alias) +| 6.5.0 through 7.3.x | `.kibana_N` (`.kibana` alias) +| 7.4.0 through 7.11.x +| `.kibana_N` (`.kibana` alias) -While {kib} is starting up and before serving any HTTP traffic, it checks to see if any internal mapping changes or data transformations for existing saved objects are required. +`.kibana_task_manager_N` (`.kibana_task_manager` alias) +|======================= -When changes are necessary, a new migration is started. To ensure that only one {kib} instance performs the migration, each instance will attempt to obtain a migration lock by creating a new `.kibana_N+1` index. The instance that succeeds in creating the index will then read batches of documents from the existing index, migrate them, and write them to the new index. Once the objects are migrated, the lock is released by pointing the `.kibana` index alias the new upgraded `.kibana_N+1` index. +==== Upgrading multiple {kib} instances +When upgrading several {kib} instances connected to the same {es} cluster, ensure that all outdated instances are shutdown before starting the upgrade. -Instances that failed to acquire a lock will log `Another Kibana instance appears to be migrating the index. Waiting for that migration to complete`. The instance will then wait until `.kibana` points to an upgraded index before starting up and serving HTTP traffic. +Kibana does not support rolling upgrades. However, once outdated instances are shutdown, all upgraded instances can be started in parallel in which case all instances will participate in the upgrade migration in parallel. -NOTE: Prior to 6.5.0, saved objects were stored directly in an index named `.kibana`. After upgrading to version 6.5+, {kib} will migrate this index into `.kibana_N` and set `.kibana` up as an index alias. + -Prior to 7.4.0, task manager tasks were stored directly in an index name `.kibana_task_manager`. After upgrading to version 7.4+, {kib} will migrate this index into `.kibana_task_manager_N` and set `.kibana_task_manager` up as an index alias. +For large deployments with more than 10 {kib} instances and more than 10 000 saved objects, the upgrade downtime can be reduced by bringing up a single {kib} instance and waiting for it to complete the upgrade migration before bringing up the remaining instances. [float] [[preventing-migration-failures]] @@ -54,50 +72,31 @@ Problems with your {es} cluster can prevent {kib} upgrades from succeeding. Ensu * a "green" cluster status [float] -===== Running different versions of {kib} connected to the same {es} index -Kibana does not support rolling upgrades. Stop all {kib} instances before starting a newer version to prevent upgrade failures and data loss. +===== Different versions of {kib} connected to the same {es} index +When different versions of {kib} are attempting an upgrade migration in parallel this can lead to migration failures. Ensure that all {kib} instances are running the same version, configuration and plugins. [float] ===== Incompatible `xpack.tasks.index` configuration setting -For {kib} < 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. +For {kib} versions prior to 7.5.1, if the task manager index is set to `.tasks` with the configuration setting `xpack.tasks.index: ".tasks"`, upgrade migrations will fail. {kib} 7.5.1 and later prevents this by refusing to start with an incompatible configuration setting. [float] [[resolve-migrations-failures]] ==== Resolving migration failures -If {kib} terminates unexpectedly while migrating a saved object index, manual intervention is required before {kib} will attempt to perform the migration again. Follow the advice in (preventing migration failures)[preventing-migration-failures] before retrying a migration upgrade. - -As mentioned above, {kib} will create a migration lock for each index that requires a migration by creating a new `.kibana_N+1` index. For example: if the `.kibana_task_manager` alias is pointing to `.kibana_task_manager_5` then the first {kib} that succeeds in creating `.kibana_task_manager_6` will obtain the lock to start migrations. - -However, if the instance that obtained the lock fails to migrate the index, all other {kib} instances will be blocked from performing this migration. This includes the instance that originally obtained the lock, it will be blocked from retrying the migration even when restarted. - -[float] -===== Retry a migration by restoring a backup snapshot: - -1. Before proceeding ensure that you have a recent and successful backup snapshot of all `.kibana*` indices. -2. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -3. Delete all saved object indices with `DELETE /.kibana*` -4. Restore the `.kibana* indices and their aliases from the backup snapshot. See {es} {ref}/modules-snapshots.html[snapshots] -5. Start up all {kib} instances to retry the upgrade migration. - -[float] -===== (Not recommended) Retry a migration without a backup snapshot: +If {kib} terminates unexpectedly while migrating a saved object index it will automatically attempt to perform the migration again once the process has restarted. Do not delete any saved objects indices to attempt to fix a failed migration. Unlike previous versions, {kib} version 7.12.0 and later does not require deleting any indices to release a failed migration lock. -1. Shutdown all {kib} instances to be 100% sure that there are no instances currently performing a migration. -2. Identify any migration locks by comparing the output of `GET /_cat/aliases` and `GET /_cat/indices`. If e.g. `.kibana` is pointing to `.kibana_4` and there is a `.kibana_5` index, the `.kibana_5` index will act like a migration lock blocking further attempts. Be sure to check both the `.kibana` and `.kibana_task_manager` aliases and their indices. -3. Remove any migration locks e.g. `DELETE /.kibana_5`. -4. Start up all {kib} instances. +If upgrade migrations fail repeatedly, follow the advice in (preventing migration failures)[preventing-migration-failures]. Once the root cause for the migration failure has been addressed, {kib} will automatically retry the migration without any further intervention. If you're unable to resolve a failed migration following these steps, please contact support. [float] [[upgrade-migrations-rolling-back]] ==== Rolling back to a previous version of {kib} -If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify the root cause. +If you've followed the advice in (preventing migration failures)[preventing-migration-failures] and (resolving migration failures)[resolve-migrations-failures] and {kib} is still not able to upgrade successfully, you might choose to rollback {kib} until you're able to identify and fix the root cause. WARNING: Before rolling back {kib}, ensure that the version you wish to rollback to is compatible with your {es} cluster. If the version you're rolling back to is not compatible, you will have to also rollback {es}. + Any changes made after an upgrade will be lost when rolling back to a previous version. -In order to rollback after a failed upgrade migration, the saved object indices might also have to be rolled back to be compatible with the previous {kibana} version. +In order to rollback after a failed upgrade migration, the saved object indices have to be rolled back to be compatible with the previous {kibana} version. [float] ===== Rollback by restoring a backup snapshot: @@ -111,17 +110,15 @@ In order to rollback after a failed upgrade migration, the saved object indices [float] ===== (Not recommended) Rollback without a backup snapshot: -WARNING: {kib} does not run a migration for every saved object index on every upgrade. A {kib} version upgrade can cause no migrations, migrate only the `.kibana` or the `.kibana_task_manager` index or both. Carefully read the logs to ensure that you're only deleting indices created by a later version of {kib} to avoid data loss. - 1. Shutdown all {kib} instances to be 100% sure that there are no {kib} instances currently performing a migration. 2. Create a backup snapshot of the `.kibana*` indices. -3. Use the logs from the upgraded instances to identify which indices {kib} attempted to upgrade. The server logs will contain an entry like `[savedobjects-service] Creating index .kibana_4.` and/or `[savedobjects-service] Creating index .kibana_task_manager_2.` If no indices were created after upgrading {kib} then no further action is required to perform a rollback, skip ahead to step (5). If you're running multiple {kib} instances, be sure to inspect all instances' logs. -4. Delete each of the indices identified in step (2). e.g. `DELETE /.kibana_task_manager_2` -5. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +3. Delete the version specific indices created by the failed upgrade migration. E.g. if you wish to rollback from a failed upgrade to v7.12.0 `DELETE /.kibana_7.12.0_*,.kibana_task_manager_7.12.0_*` +4. Inspect the output of `GET /_cat/aliases`. If either the `.kibana` and/or `.kibana_task_manager` alias is missing, these will have to be created manually. Find the latest index from the output of `GET /_cat/indices` and create the missing alias to point to the latest index. E.g. if the `.kibana` alias was missing and the latest index is `.kibana_3` create a new alias with `POST /.kibana_3/_aliases/.kibana`. +5. Remove the write block from the rollback indices. `PUT /.kibana,.kibana_task_manager/_settings {"index.blocks.write": false}` 6. Start up {kib} on the older version you wish to rollback to. [float] [[upgrade-migrations-old-indices]] ==== Handling old `.kibana_N` indices -After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, etc). {kib} only uses the index that the `.kibana` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file +After migrations have completed, there will be multiple {kib} indices in {es}: (`.kibana_1`, `.kibana_2`, `.kibana_7.12.0` etc). {kib} only uses the index that the `.kibana` and `.kibana_task_manager` alias points to. The other {kib} indices can be safely deleted, but are left around as a matter of historical record, and to facilitate rolling {kib} back to a previous version. \ No newline at end of file diff --git a/docs/setup/upgrade/upgrade-standard.asciidoc b/docs/setup/upgrade/upgrade-standard.asciidoc index b27bb8867e624..b43da6aef9765 100644 --- a/docs/setup/upgrade/upgrade-standard.asciidoc +++ b/docs/setup/upgrade/upgrade-standard.asciidoc @@ -15,17 +15,17 @@ necessary remediation steps as per those instructions. [float] ==== Upgrading multiple {kib} instances -WARNING: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. +NOTE: Kibana does not support rolling upgrades. If you're running multiple {kib} instances, all instances should be stopped before upgrading. -Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause upgrade migration failures and data loss. This is because acknowledged writes from the older instances could be written into the _old_ index while the migration is in progress. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. - -The first instance that triggers saved object migrations will run the entire process. Any other instances started up while a migration is running will log a message and then wait until saved object migrations has completed before they start serving HTTP traffic. +Different versions of {kib} running against the same {es} index, such as during a rolling upgrade, can cause data loss. This is because older instances will continue to write saved objects in a different format than the newer instances. To prevent this from happening ensure that all old {kib} instances are shutdown before starting up instances on a newer version. [float] ==== Upgrade using a `deb` or `rpm` package . Stop the existing {kib} process using the appropriate command for your - system. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. + system. If you have multiple {kib} instances connecting to the same {es} + cluster ensure that all instances are stopped before proceeding to the next + step to avoid data loss. . Use `rpm` or `dpkg` to install the new package. All files should be placed in their proper locations and config files should not be overwritten. + @@ -65,5 +65,7 @@ and becomes a new instance in the monitoring data. . Install the appropriate versions of all your plugins for your new installation using the `kibana-plugin` script. Check out the <> documentation for more information. -. Stop the old {kib} process. If you have multiple {kib} instances connecting to the same {es} cluster ensure that all instances are stopped before proceeding to the next step to avoid data loss. +. Stop the old {kib} process. If you have multiple {kib} instances connecting + to the same {es} cluster ensure that all instances are stopped before + proceeding to the next step to avoid data loss. . Start the new {kib} process. diff --git a/examples/expressions_explorer/README.md b/examples/expressions_explorer/README.md new file mode 100644 index 0000000000000..ead0ca758f8e5 --- /dev/null +++ b/examples/expressions_explorer/README.md @@ -0,0 +1,8 @@ +## expressions explorer + +This example expressions explorer app shows how to: + - to run expression + - to render expression output + - emit events from expression renderer and handle them + +To run this example, use the command `yarn start --run-examples`. \ No newline at end of file diff --git a/examples/expressions_explorer/kibana.json b/examples/expressions_explorer/kibana.json new file mode 100644 index 0000000000000..038b7eea0ef21 --- /dev/null +++ b/examples/expressions_explorer/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "expressionsExplorer", + "version": "0.0.1", + "kibanaVersion": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["expressions", "inspector", "uiActions", "developerExamples"], + "optionalPlugins": [], + "requiredBundles": [] +} diff --git a/examples/expressions_explorer/public/actions/navigate_action.ts b/examples/expressions_explorer/public/actions/navigate_action.ts new file mode 100644 index 0000000000000..d29a9e6b345b6 --- /dev/null +++ b/examples/expressions_explorer/public/actions/navigate_action.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { createAction } from '../../../../src/plugins/ui_actions/public'; + +export const ACTION_NAVIGATE = 'ACTION_NAVIGATE'; + +export const createNavigateAction = () => + createAction({ + id: ACTION_NAVIGATE, + type: ACTION_NAVIGATE, + getDisplayName: () => 'Navigate', + execute: async (event: any) => { + window.location.href = event.href; + }, + }); diff --git a/examples/expressions_explorer/public/actions/navigate_trigger.ts b/examples/expressions_explorer/public/actions/navigate_trigger.ts new file mode 100644 index 0000000000000..eacbd968eaa93 --- /dev/null +++ b/examples/expressions_explorer/public/actions/navigate_trigger.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Trigger } from '../../../../src/plugins/ui_actions/public'; + +export const NAVIGATE_TRIGGER_ID = 'NAVIGATE_TRIGGER_ID'; + +export const navigateTrigger: Trigger = { + id: NAVIGATE_TRIGGER_ID, +}; diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx new file mode 100644 index 0000000000000..6e2eebcde4a0f --- /dev/null +++ b/examples/expressions_explorer/public/actions_and_expressions.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger'; + +interface Props { + expressions: ExpressionsStart; + actions: UiActionsStart; +} + +export function ActionsExpressionsExample({ expressions, actions }: Props) { + const [expression, updateExpression] = useState( + 'button name="click me" href="http://www.google.com"' + ); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + const handleEvents = (event: any) => { + if (event.id !== 'NAVIGATE') return; + // enrich event context with some extra data + event.baseUrl = 'http://www.google.com'; + + actions.executeTriggerActions(NAVIGATE_TRIGGER_ID, event.value); + }; + + return ( + + + + +

Actions from expression renderers

+
+
+
+ + + + + + Here you can play with sample `button` which takes a url as configuration and + displays a button which emits custom BUTTON_CLICK trigger to which we have attached + a custom action which performs the navigation. + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+
+ ); +} diff --git a/examples/expressions_explorer/public/app.tsx b/examples/expressions_explorer/public/app.tsx new file mode 100644 index 0000000000000..d72cf08128a5a --- /dev/null +++ b/examples/expressions_explorer/public/app.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { + EuiPage, + EuiPageHeader, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiSpacer, + EuiText, + EuiLink, +} from '@elastic/eui'; +import { AppMountParameters } from '../../../src/core/public'; +import { ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; +import { RunExpressionsExample } from './run_expressions'; +import { RenderExpressionsExample } from './render_expressions'; +import { ActionsExpressionsExample } from './actions_and_expressions'; +import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; + actions: UiActionsStart; +} + +const ExpressionsExplorer = ({ expressions, inspector, actions }: Props) => { + return ( + + + Expressions Explorer + + + +

+ There are a couple of ways to run the expressions. Below some of the options are + demonstrated. You can read more about it{' '} + + here + +

+
+ + + + + + + + + + + + +
+
+
+
+ ); +}; + +export const renderApp = (props: Props, { element }: AppMountParameters) => { + ReactDOM.render(, element); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/examples/expressions_explorer/public/editor/expression_editor.tsx b/examples/expressions_explorer/public/editor/expression_editor.tsx new file mode 100644 index 0000000000000..e3dbb5998b92e --- /dev/null +++ b/examples/expressions_explorer/public/editor/expression_editor.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { EuiCodeEditor } from '@elastic/eui'; + +interface Props { + value: string; + onChange: (value: string) => void; +} + +export function ExpressionEditor({ value, onChange }: Props) { + return ( + {}} + aria-label="Code Editor" + /> + ); +} diff --git a/examples/expressions_explorer/public/functions/button.ts b/examples/expressions_explorer/public/functions/button.ts new file mode 100644 index 0000000000000..8c39aa2743b30 --- /dev/null +++ b/examples/expressions_explorer/public/functions/button.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from '../../../../src/plugins/expressions/common'; + +interface Arguments { + href: string; + name: string; +} + +export type ExpressionFunctionButton = ExpressionFunctionDefinition< + 'button', + unknown, + Arguments, + unknown +>; + +export const buttonFn: ExpressionFunctionButton = { + name: 'button', + args: { + href: { + help: i18n.translate('expressions.functions.font.args.href', { + defaultMessage: 'Link to which to navigate', + }), + types: ['string'], + required: true, + }, + name: { + help: i18n.translate('expressions.functions.font.args.name', { + defaultMessage: 'Name of the button', + }), + types: ['string'], + default: 'button', + }, + }, + help: 'Configures the button', + fn: (input: unknown, args: Arguments) => { + return { + type: 'render', + as: 'button', + value: args, + }; + }, +}; diff --git a/examples/expressions_explorer/public/index.ts b/examples/expressions_explorer/public/index.ts new file mode 100644 index 0000000000000..a6dbbc9198f44 --- /dev/null +++ b/examples/expressions_explorer/public/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ExpressionsExplorerPlugin } from './plugin'; + +export const plugin = () => new ExpressionsExplorerPlugin(); diff --git a/examples/expressions_explorer/public/inspector/ast_debug_view.tsx b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx new file mode 100644 index 0000000000000..d860ff30bd8e9 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/ast_debug_view.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { EuiTreeView, EuiDescriptionList, EuiCodeBlock, EuiText, EuiSpacer } from '@elastic/eui'; + +interface Props { + ast: any; +} + +const decorateAst = (ast: any, nodeClicked: any) => { + return ast.chain.map((link: any) => { + return { + id: link.function + Math.random(), + label: link.function, + callback: () => { + nodeClicked(link.debug); + }, + children: Object.keys(link.arguments).reduce((result: any, key: string) => { + if (typeof link.arguments[key] === 'object') { + // result[key] = decorateAst(link.arguments[key]); + } + return result; + }, []), + }; + }); +}; + +const prepareNode = (key: string, value: any) => { + if (key === 'args') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'output' || key === 'input') { + return ( + + {JSON.stringify(value, null, '\t')} + + ); + } else if (key === 'success') { + return value ? 'true' : 'false'; + } else return {value}; +}; + +export function AstDebugView({ ast }: Props) { + const [nodeInfo, setNodeInfo] = useState([] as any[]); + const items = decorateAst(ast, (node: any) => { + setNodeInfo( + Object.keys(node).map((key) => ({ + title: key, + description: prepareNode(key, node[key]), + })) + ); + }); + + return ( +
+ List of executed expression functions: + + + Details of selected function: + +
+ ); +} diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx new file mode 100644 index 0000000000000..1233735072d04 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { InspectorViewProps, Adapters } from '../../../../src/plugins/inspector/public'; +import { AstDebugView } from './ast_debug_view'; + +interface ExpressionsInspectorViewComponentState { + ast: any; + adapters: Adapters; +} + +class ExpressionsInspectorViewComponent extends Component< + InspectorViewProps, + ExpressionsInspectorViewComponentState +> { + static propTypes = { + adapters: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + }; + + state = {} as ExpressionsInspectorViewComponentState; + + static getDerivedStateFromProps( + nextProps: Readonly, + state: ExpressionsInspectorViewComponentState + ) { + if (state && nextProps.adapters === state.adapters) { + return null; + } + + const { ast } = nextProps.adapters.expression; + + return { + adapters: nextProps.adapters, + ast, + }; + } + + onUpdateData = (ast: any) => { + this.setState({ + ast, + }); + }; + + componentDidMount() { + this.props.adapters.expression!.on('change', this.onUpdateData); + } + + componentWillUnmount() { + this.props.adapters.expression!.removeListener('change', this.onUpdateData); + } + + static renderNoData() { + return ( + + + + } + body={ + +

+ +

+
+ } + /> + ); + } + + render() { + if (!this.state.ast) { + return ExpressionsInspectorViewComponent.renderNoData(); + } + + return ; + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default ExpressionsInspectorViewComponent; diff --git a/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx new file mode 100644 index 0000000000000..b10c82e5df309 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/expressions_inspector_view_wrapper.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { lazy } from 'react'; + +const ExpressionsInspectorViewComponent = lazy(() => import('./expressions_inspector_view')); + +export const getExpressionsInspectorViewComponentWrapper = () => { + return (props: any) => { + return ; + }; +}; diff --git a/examples/expressions_explorer/public/inspector/index.ts b/examples/expressions_explorer/public/inspector/index.ts new file mode 100644 index 0000000000000..ec87a1240ac74 --- /dev/null +++ b/examples/expressions_explorer/public/inspector/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { Adapters, InspectorViewDescription } from '../../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewComponentWrapper } from './expressions_inspector_view_wrapper'; + +export const getExpressionsInspectorViewDescription = (): InspectorViewDescription => ({ + title: i18n.translate('data.inspector.table.dataTitle', { + defaultMessage: 'Expression', + }), + order: 100, + help: i18n.translate('data.inspector.table..dataDescriptionTooltip', { + defaultMessage: 'View the expression behind the visualization', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.expression); + }, + component: getExpressionsInspectorViewComponentWrapper(), +}); diff --git a/examples/expressions_explorer/public/plugin.tsx b/examples/expressions_explorer/public/plugin.tsx new file mode 100644 index 0000000000000..9643389ad881c --- /dev/null +++ b/examples/expressions_explorer/public/plugin.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { Plugin, CoreSetup, AppMountParameters, AppNavLinkStatus } from '../../../src/core/public'; +import { DeveloperExamplesSetup } from '../../developer_examples/public'; +import { ExpressionsSetup, ExpressionsStart } from '../../../src/plugins/expressions/public'; +import { + Setup as InspectorSetup, + Start as InspectorStart, +} from '../../../src/plugins/inspector/public'; +import { getExpressionsInspectorViewDescription } from './inspector'; +import { UiActionsStart, UiActionsSetup } from '../../../src/plugins/ui_actions/public'; +import { NAVIGATE_TRIGGER_ID, navigateTrigger } from './actions/navigate_trigger'; +import { ACTION_NAVIGATE, createNavigateAction } from './actions/navigate_action'; +import { buttonRenderer } from './renderers/button'; +import { buttonFn } from './functions/button'; + +interface StartDeps { + expressions: ExpressionsStart; + inspector: InspectorStart; + uiActions: UiActionsStart; +} + +interface SetupDeps { + uiActions: UiActionsSetup; + expressions: ExpressionsSetup; + inspector: InspectorSetup; + developerExamples: DeveloperExamplesSetup; +} + +export class ExpressionsExplorerPlugin implements Plugin { + public setup(core: CoreSetup, deps: SetupDeps) { + // register custom inspector adapter & view + deps.inspector.registerView(getExpressionsInspectorViewDescription()); + + // register custom actions + deps.uiActions.registerTrigger(navigateTrigger); + deps.uiActions.registerAction(createNavigateAction()); + deps.uiActions.attachAction(NAVIGATE_TRIGGER_ID, ACTION_NAVIGATE); + + // register custom functions and renderers + deps.expressions.registerRenderer(buttonRenderer); + deps.expressions.registerFunction(buttonFn); + + core.application.register({ + id: 'expressionsExplorer', + title: 'Expressions Explorer', + navLinkStatus: AppNavLinkStatus.hidden, + async mount(params: AppMountParameters) { + const [, depsStart] = await core.getStartServices(); + const { renderApp } = await import('./app'); + return renderApp( + { + expressions: depsStart.expressions, + inspector: depsStart.inspector, + actions: depsStart.uiActions, + }, + params + ); + }, + }); + + deps.developerExamples.register({ + appId: 'expressionsExplorer', + title: 'Expressions', + description: `Expressions is a plugin that allows to execute Kibana expressions and render content using expression renderers. This example plugin showcases various usage scenarios.`, + links: [ + { + label: 'README', + href: 'https://github.com/elastic/kibana/blob/master/src/plugins/expressions/README.md', + iconType: 'logoGithub', + size: 's', + target: '_blank', + }, + ], + }); + } + + public start() {} + + public stop() {} +} diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx new file mode 100644 index 0000000000000..ffbe558f30218 --- /dev/null +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState } from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ReactExpressionRenderer, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RenderExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer rendering"'); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = { + expression: new ExpressionsInspectorAdapter(), + }; + + return ( + + + + +

Render expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and render it. Using + ReactExpressionRenderer component makes that very easy. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + { + return
{message}
; + }} + /> +
+
+
+
+
+
+ ); +} diff --git a/examples/expressions_explorer/public/renderers/button.tsx b/examples/expressions_explorer/public/renderers/button.tsx new file mode 100644 index 0000000000000..32f1f31894dce --- /dev/null +++ b/examples/expressions_explorer/public/renderers/button.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import ReactDOM from 'react-dom'; +import React from 'react'; +import { EuiButton } from '@elastic/eui'; +import { ExpressionRenderDefinition } from '../../../../src/plugins/expressions/common/expression_renderers'; + +export const buttonRenderer: ExpressionRenderDefinition = { + name: 'button', + displayName: 'Button', + reuseDomNode: true, + render(domNode, config, handlers) { + const buttonClick = () => { + handlers.event({ + id: 'NAVIGATE', + value: { + href: config.href, + }, + }); + }; + + const renderDebug = () => ( +
+ + {config.name} + +
+ ); + + ReactDOM.render(renderDebug(), domNode, () => handlers.done()); + + handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); + }, +}; diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx new file mode 100644 index 0000000000000..efbdbc2d41836 --- /dev/null +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React, { useState, useEffect, useMemo } from 'react'; +import { + EuiCodeBlock, + EuiFlexItem, + EuiFlexGroup, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPanel, + EuiText, + EuiTitle, + EuiButton, +} from '@elastic/eui'; +import { + ExpressionsStart, + ExpressionsInspectorAdapter, +} from '../../../src/plugins/expressions/public'; +import { ExpressionEditor } from './editor/expression_editor'; +import { Start as InspectorStart } from '../../../src/plugins/inspector/public'; + +interface Props { + expressions: ExpressionsStart; + inspector: InspectorStart; +} + +export function RunExpressionsExample({ expressions, inspector }: Props) { + const [expression, updateExpression] = useState('markdown "## expressions explorer"'); + const [result, updateResult] = useState({}); + + const expressionChanged = (value: string) => { + updateExpression(value); + }; + + const inspectorAdapters = useMemo( + () => ({ + expression: new ExpressionsInspectorAdapter(), + }), + [] + ); + + useEffect(() => { + const runExpression = async () => { + const execution = expressions.execute(expression, null, { + debug: true, + inspectorAdapters, + }); + + const data: any = await execution.getData(); + updateResult(data); + }; + + runExpression(); + }, [expression, expressions, inspectorAdapters]); + + return ( + + + + +

Run expressions

+
+
+
+ + + + + + In the below editor you can enter your expression and execute it. Using + expressions.execute allows you to easily run the expression. + + + + { + inspector.open(inspectorAdapters); + }} + > + Open Inspector + + + + + + + + + + + + + + {JSON.stringify(result, null, '\t')} + + + + + + +
+ ); +} diff --git a/examples/expressions_explorer/tsconfig.json b/examples/expressions_explorer/tsconfig.json new file mode 100644 index 0000000000000..b4449819b25a6 --- /dev/null +++ b/examples/expressions_explorer/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../typings/**/*", + ], + "exclude": [], + "references": [ + { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/kibana_react/tsconfig.json" }, + ] +} diff --git a/package.json b/package.json index 87e0f84695235..24297011ccc63 100644 --- a/package.json +++ b/package.json @@ -287,7 +287,7 @@ "react-resizable": "^1.7.5", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-use": "^13.27.0", + "react-use": "^15.3.4", "recompose": "^0.26.0", "redux": "^4.0.5", "redux-actions": "^2.6.5", @@ -828,7 +828,7 @@ "url-loader": "^2.2.0", "use-resize-observer": "^6.0.0", "val-loader": "^1.1.1", - "vega": "^5.18.0", + "vega": "^5.19.1", "vega-lite": "^4.17.0", "vega-schema-url-parser": "^2.1.0", "vega-tooltip": "^0.25.0", diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 741f715ba6ebe..6ba652abda3d5 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -206,6 +206,20 @@ describe('DocumentMigrator', () => { ); }); + it('coerces the current Kibana version if it has a hyphen', () => { + const validDefinition = { + kibanaVersion: '3.2.0-SNAPSHOT', + typeRegistry: createRegistry({ + name: 'foo', + convertToMultiNamespaceTypeVersion: '3.2.0', + namespaceType: 'multiple', + }), + minimumConvertVersion: '0.0.0', + log: mockLogger, + }; + expect(() => new DocumentMigrator(validDefinition)).not.toThrowError(); + }); + it('validates convertToMultiNamespaceTypeVersion is not used on a patch version', () => { const invalidDefinition = { kibanaVersion: '3.2.3', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index e4b89a949d3cf..e93586ec7ce4c 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -159,10 +159,11 @@ export class DocumentMigrator implements VersionedTransformer { */ constructor({ typeRegistry, - kibanaVersion, + kibanaVersion: rawKibanaVersion, minimumConvertVersion = DEFAULT_MINIMUM_CONVERT_VERSION, log, }: DocumentMigratorOptions) { + const kibanaVersion = rawKibanaVersion.split('-')[0]; // coerce a semver-like string (x.y.z-SNAPSHOT) or prerelease version (x.y.z-alpha) to a regular semver (x.y.z) validateMigrationDefinition(typeRegistry, kibanaVersion, minimumConvertVersion); this.documentMigratorOptions = { typeRegistry, kibanaVersion, log }; diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 20865bea2f897..7c28db333cc83 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -30,6 +30,9 @@ export interface BrushTriggerEvent { type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + /** * returns accessor value from string or function accessor * @param datum @@ -82,6 +85,29 @@ const getAllSplitAccessors = ( value, ]); +/** + * Gets value from small multiple accessors + * + * Only handles single small multiple accessor + */ +function getSplitChartValue({ + smHorizontalAccessorValue, + smVerticalAccessorValue, +}: Pick): + | string + | number + | undefined { + if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smHorizontalAccessorValue; + } + + if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + return smVerticalAccessorValue; + } + + return; +} + /** * Reduces matching column indexes * @@ -92,7 +118,8 @@ const getAllSplitAccessors = ( const columnReducer = ( xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn ) => ( acc: Array<[index: number, id: string]>, { id }: Datatable['columns'][number], @@ -101,6 +128,7 @@ const columnReducer = ( if ( (xAccessor !== null && validateAccessorId(id, xAccessor)) || (yAccessor !== null && validateAccessorId(id, yAccessor)) || + (splitChartAccessor !== undefined && validateAccessorId(id, splitChartAccessor)) || splitAccessors.some(([accessor]) => validateAccessorId(id, accessor)) ) { acc.push([index, id]); @@ -121,13 +149,18 @@ const rowFindPredicate = ( geometry: GeometryValue | null, xAccessor: Accessor | AccessorFn | null, yAccessor: Accessor | AccessorFn | null, - splitAccessors: AllSeriesAccessors + splitAccessors: AllSeriesAccessors, + splitChartAccessor?: Accessor | AccessorFn, + splitChartValue?: string | number ) => (row: Datatable['rows'][number]): boolean => (geometry === null || (xAccessor !== null && getAccessorValue(row, xAccessor) === geometry.x && yAccessor !== null && - getAccessorValue(row, yAccessor) === geometry.y)) && + getAccessorValue(row, yAccessor) === geometry.y && + (splitChartAccessor === undefined || + (splitChartValue !== undefined && + getAccessorValue(row, splitChartAccessor) === splitChartValue)))) && [...splitAccessors].every(([accessor, value]) => getAccessorValue(row, accessor) === value); /** @@ -142,19 +175,28 @@ export const getFilterFromChartClickEventFn = ( table: Datatable, xAccessor: Accessor | AccessorFn, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate: boolean = false ) => (points: Array<[GeometryValue, XYChartSeriesIdentifier]>): ClickTriggerEvent => { const data: ValueClickContext['data']['data'] = []; points.forEach((point) => { const [geometry, { yAccessor, splitAccessors }] = point; + const splitChartValue = getSplitChartValue(point[1]); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(xAccessor, yAccessor, allSplitAccessors), + columnReducer(xAccessor, yAccessor, allSplitAccessors, splitChartAccessor), [] ); const row = table.rows.findIndex( - rowFindPredicate(geometry, xAccessor, yAccessor, allSplitAccessors) + rowFindPredicate( + geometry, + xAccessor, + yAccessor, + allSplitAccessors, + splitChartAccessor, + splitChartValue + ) ); const newData = columns.map(([column, id]) => ({ table, @@ -179,16 +221,20 @@ export const getFilterFromChartClickEventFn = ( * Helper function to get filter action event from series */ export const getFilterFromSeriesFn = (table: Datatable) => ( - { splitAccessors }: XYChartSeriesIdentifier, + { splitAccessors, ...rest }: XYChartSeriesIdentifier, splitSeriesAccessorFnMap?: Map, + splitChartAccessor?: Accessor | AccessorFn, negate = false ): ClickTriggerEvent => { + const splitChartValue = getSplitChartValue(rest); const allSplitAccessors = getAllSplitAccessors(splitAccessors, splitSeriesAccessorFnMap); const columns = table.columns.reduce>( - columnReducer(null, null, allSplitAccessors), + columnReducer(null, null, allSplitAccessors, splitChartAccessor), [] ); - const row = table.rows.findIndex(rowFindPredicate(null, null, null, allSplitAccessors)); + const row = table.rows.findIndex( + rowFindPredicate(null, null, null, allSplitAccessors, splitChartAccessor, splitChartValue) + ); const data: ValueClickContext['data']['data'] = columns.map(([column, id]) => ({ table, column, diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.test.ts b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts new file mode 100644 index 0000000000000..56db5346b7c6c --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/session_restoration.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { dataPluginMock } from '../../../../data/public/mocks'; +import { createSessionRestorationDataProvider } from './session_restoration'; +import { getAppStateDefaults } from './get_app_state_defaults'; +import { getSavedDashboardMock } from '../test_helpers'; +import { SavedObjectTagDecoratorTypeGuard } from '../../../../saved_objects_tagging_oss/public'; + +describe('createSessionRestorationDataProvider', () => { + const mockDataPlugin = dataPluginMock.createStartContract(); + const searchSessionInfoProvider = createSessionRestorationDataProvider({ + data: mockDataPlugin, + getAppState: () => + getAppStateDefaults( + getSavedDashboardMock(), + false, + ((() => false) as unknown) as SavedObjectTagDecoratorTypeGuard + ), + getDashboardTitle: () => 'Dashboard', + getDashboardId: () => 'Id', + }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + + test('restoreState has refreshInterval paused', async () => { + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.refreshInterval).toBeUndefined(); + expect(restoreState.refreshInterval?.pause).toBe(true); + }); + }); +}); diff --git a/src/plugins/dashboard/public/application/lib/session_restoration.ts b/src/plugins/dashboard/public/application/lib/session_restoration.ts index 60a0c56a63218..fb57f8caa5ce4 100644 --- a/src/plugins/dashboard/public/application/lib/session_restoration.ts +++ b/src/plugins/dashboard/public/application/lib/session_restoration.ts @@ -21,8 +21,8 @@ export function createSessionRestorationDataProvider(deps: { getUrlGeneratorData: async () => { return { urlGeneratorId: DASHBOARD_APP_URL_GENERATOR, - initialState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: false }), - restoreState: getUrlGeneratorState({ ...deps, forceAbsoluteTime: true }), + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), }; }, }; @@ -32,20 +32,17 @@ function getUrlGeneratorState({ data, getAppState, getDashboardId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { data: DataPublicPluginStart; getAppState: () => DashboardAppState; getDashboardId: () => string; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DashboardUrlGeneratorState { const appState = getAppState(); return { dashboardId: getDashboardId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), filters: data.query.filterManager.getFilters(), @@ -55,6 +52,12 @@ function getUrlGeneratorState({ preserveSavedFilters: false, viewMode: appState.viewMode, panels: getDashboardId() ? undefined : appState.panels, - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, + refreshInterval: shouldRestoreSearchSession + ? { + pause: true, // force pause refresh interval when restoring a session + value: 0, + } + : undefined, }; } diff --git a/src/plugins/discover/public/application/angular/directives/histogram.tsx b/src/plugins/discover/public/application/angular/directives/histogram.tsx index ff10feea46d47..b12de3f4496c5 100644 --- a/src/plugins/discover/public/application/angular/directives/histogram.tsx +++ b/src/plugins/discover/public/application/angular/directives/histogram.tsx @@ -154,6 +154,10 @@ export class DiscoverHistogram extends Component xAxisFormatter.convert(value)} /> { // show all the Rows $scope.minimumVisibleRows = $scope.hits; // delay scrolling to after the rows have been rendered - const bottomMarker = $element.find('#discoverBottomMarker'); - $timeout(() => { - bottomMarker.focus(); - // The anchor tag is not technically empty (it's a hack to make Safari scroll) - // so the browser will show a highlight: remove the focus once scrolled - $timeout(() => { - bottomMarker.blur(); - }, 0); - }, 0); + const bottomMarker = document.getElementById('discoverBottomMarker'); + const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + while ($scope.rows.length !== document.getElementsByClassName('kbnDocTable__row').length) { + await wait(50); + } + bottomMarker.focus(); + await wait(50); + bottomMarker.blur(); }; $scope.newQuery = function () { diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index 45e5e252e8361..809664de5f073 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -101,8 +101,9 @@ describe('Test discover state with legacy migration', () => { describe('createSearchSessionRestorationDataProvider', () => { let mockSavedSearch: SavedSearch = ({} as unknown) as SavedSearch; + const mockDataPlugin = dataPluginMock.createStartContract(); const searchSessionInfoProvider = createSearchSessionRestorationDataProvider({ - data: dataPluginMock.createStartContract(), + data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), }).appStateContainer, @@ -124,4 +125,30 @@ describe('createSearchSessionRestorationDataProvider', () => { expect(await searchSessionInfoProvider.getName()).toBe('Discover'); }); }); + + describe('session state', () => { + test('restoreState has sessionId and initialState has not', async () => { + const searchSessionId = 'id'; + (mockDataPlugin.search.session.getSessionId as jest.Mock).mockImplementation( + () => searchSessionId + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.searchSessionId).toBeUndefined(); + expect(restoreState.searchSessionId).toBe(searchSessionId); + }); + + test('restoreState has absoluteTimeRange', async () => { + const relativeTime = 'relativeTime'; + const absoluteTime = 'absoluteTime'; + (mockDataPlugin.query.timefilter.timefilter.getTime as jest.Mock).mockImplementation( + () => relativeTime + ); + (mockDataPlugin.query.timefilter.timefilter.getAbsoluteTime as jest.Mock).mockImplementation( + () => absoluteTime + ); + const { initialState, restoreState } = await searchSessionInfoProvider.getUrlGeneratorData(); + expect(initialState.timeRange).toBe(relativeTime); + expect(restoreState.timeRange).toBe(absoluteTime); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index fe05fceb858e5..c769e263655ab 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -275,12 +275,12 @@ export function createSearchSessionRestorationDataProvider(deps: { initialState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: false, + shouldRestoreSearchSession: false, }), restoreState: createUrlGeneratorState({ ...deps, getSavedSearchId, - forceAbsoluteTime: true, + shouldRestoreSearchSession: true, }), }; }, @@ -291,15 +291,12 @@ function createUrlGeneratorState({ appStateContainer, data, getSavedSearchId, - forceAbsoluteTime, + shouldRestoreSearchSession, }: { appStateContainer: StateContainer; data: DataPublicPluginStart; getSavedSearchId: () => string | undefined; - /** - * Can force time range from time filter to convert from relative to absolute time range - */ - forceAbsoluteTime: boolean; + shouldRestoreSearchSession: boolean; }): DiscoverUrlGeneratorState { const appState = appStateContainer.get(); return { @@ -307,10 +304,10 @@ function createUrlGeneratorState({ indexPatternId: appState.index, query: appState.query, savedSearchId: getSavedSearchId(), - timeRange: forceAbsoluteTime + timeRange: shouldRestoreSearchSession ? data.query.timefilter.timefilter.getAbsoluteTime() : data.query.timefilter.timefilter.getTime(), - searchSessionId: data.search.session.getSessionId(), + searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined, columns: appState.columns, sort: appState.sort, savedQuery: appState.savedQuery, diff --git a/src/plugins/discover/public/application/angular/helpers/point_series.ts b/src/plugins/discover/public/application/angular/helpers/point_series.ts index db58aebfd3ad7..89317f77ef060 100644 --- a/src/plugins/discover/public/application/angular/helpers/point_series.ts +++ b/src/plugins/discover/public/application/angular/helpers/point_series.ts @@ -62,6 +62,7 @@ export interface Chart { }>; xAxisOrderedValues: number[]; xAxisFormat: Dimension['format']; + yAxisFormat: Dimension['format']; xAxisLabel: Column['name']; yAxisLabel?: Column['name']; ordered: Ordered; @@ -76,7 +77,7 @@ export const buildPointSeriesData = (table: Table, dimensions: Dimensions) => { chart.xAxisOrderedValues = uniq(table.rows.map((r) => r[xAccessor] as number)); chart.xAxisFormat = x.format; chart.xAxisLabel = table.columns[x.accessor].name; - + chart.yAxisFormat = y.format; const { intervalESUnit, intervalESValue, interval, bounds } = x.params; chart.ordered = { date: true, diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 8e068818ec0ce..0240ec90cb1e6 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -29,6 +29,7 @@ import { getByAlias } from '../util/get_by_alias'; import { ExecutionContract } from './execution_contract'; import { ExpressionExecutionParams } from '../service'; import { TablesAdapter } from '../util/tables_adapter'; +import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter'; /** * AbortController is not available in Node until v15, so we @@ -63,6 +64,7 @@ export interface ExecutionParams { const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({ requests: new RequestAdapter(), tables: new TablesAdapter(), + expression: new ExpressionsInspectorAdapter(), }); export class Execution< @@ -208,6 +210,9 @@ export class Execution< this.firstResultFuture.promise .then( (result) => { + if (this.context.inspectorAdapters.expression) { + this.context.inspectorAdapters.expression.logAST(this.state.get().ast); + } this.state.transitions.setResult(result); }, (error) => { diff --git a/src/plugins/expressions/common/util/expressions_inspector_adapter.ts b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts new file mode 100644 index 0000000000000..c82884d373d2f --- /dev/null +++ b/src/plugins/expressions/common/util/expressions_inspector_adapter.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { EventEmitter } from 'events'; + +export class ExpressionsInspectorAdapter extends EventEmitter { + private _ast: any = {}; + + public logAST(ast: any): void { + this._ast = ast; + this.emit('change', this._ast); + } + + public get ast() { + return this._ast; + } +} diff --git a/src/plugins/expressions/common/util/index.ts b/src/plugins/expressions/common/util/index.ts index ecb7d5cdca81e..4762f9979fe4a 100644 --- a/src/plugins/expressions/common/util/index.ts +++ b/src/plugins/expressions/common/util/index.ts @@ -9,3 +9,4 @@ export * from './create_error'; export * from './get_by_alias'; export * from './tables_adapter'; +export * from './expressions_inspector_adapter'; diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 9485daf49c981..d6dd2fc1f3d37 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -107,4 +107,5 @@ export { ExpressionsServiceSetup, ExpressionsServiceStart, TablesAdapter, + ExpressionsInspectorAdapter, } from '../common'; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 7fa0857be8aba..029d727e82e74 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -551,6 +551,16 @@ export class ExpressionRenderHandler { update$: Observable; } +// Warning: (ae-missing-release-tag) "ExpressionsInspectorAdapter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class ExpressionsInspectorAdapter extends EventEmitter { + // (undocumented) + get ast(): any; + // (undocumented) + logAST(ast: any): void; +} + // Warning: (ae-missing-release-tag) "ExpressionsPublicPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/saved_objects_management/public/lib/create_field_list.ts b/src/plugins/saved_objects_management/public/lib/create_field_list.ts index cd30a02bd0ef3..4497fb04ffa2c 100644 --- a/src/plugins/saved_objects_management/public/lib/create_field_list.ts +++ b/src/plugins/saved_objects_management/public/lib/create_field_list.ts @@ -11,11 +11,12 @@ import { SimpleSavedObject } from '../../../../core/public'; import { castEsToKbnFieldTypeName } from '../../../data/public'; import { ObjectField } from '../management_section/types'; import { SavedObjectLoader } from '../../../saved_objects/public'; +import { SavedObjectWithMetadata } from '../types'; const maxRecursiveIterations = 20; export function createFieldList( - object: SimpleSavedObject, + object: SimpleSavedObject | SavedObjectWithMetadata, service?: SavedObjectLoader ): ObjectField[] { let fields = Object.entries(object.attributes as Record).reduce( diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index 96a4a24f6591e..e048b92b9566c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -19,14 +19,15 @@ import { set } from '@elastic/safer-lodash-set'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; +import { SavedObjectsClientContract } from '../../../../../../core/public'; import { SavedObjectLoader } from '../../../../../saved_objects/public'; import { Field } from './field'; import { ObjectField, FieldState, SubmittedFormData } from '../../types'; import { createFieldList } from '../../../lib'; +import { SavedObjectWithMetadata } from '../../../types'; interface FormProps { - object: SimpleSavedObject; + object: SavedObjectWithMetadata; service: SavedObjectLoader; savedObjectsClient: SavedObjectsClientContract; editionEnabled: boolean; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx index 31c0a76e16f58..3343e0a63f54c 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/saved_object_view.tsx @@ -14,16 +14,18 @@ import { SavedObjectsClientContract, OverlayStart, NotificationsStart, - SimpleSavedObject, ScopedHistory, + HttpSetup, } from '../../../../../core/public'; import { ISavedObjectsManagementServiceRegistry } from '../../services'; import { Header, NotFoundErrors, Intro, Form } from './components'; -import { canViewInApp } from '../../lib'; +import { canViewInApp, findObject } from '../../lib'; import { SubmittedFormData } from '../types'; +import { SavedObjectWithMetadata } from '../../types'; interface SavedObjectEditionProps { id: string; + http: HttpSetup; serviceName: string; serviceRegistry: ISavedObjectsManagementServiceRegistry; capabilities: Capabilities; @@ -36,7 +38,7 @@ interface SavedObjectEditionProps { interface SavedObjectEditionState { type: string; - object?: SimpleSavedObject; + object?: SavedObjectWithMetadata; } export class SavedObjectEdition extends Component< @@ -56,9 +58,9 @@ export class SavedObjectEdition extends Component< } componentDidMount() { - const { id, savedObjectsClient } = this.props; + const { http, id } = this.props; const { type } = this.state; - savedObjectsClient.get(type, id).then((object) => { + findObject(http, type, id).then((object) => { this.setState({ object, }); @@ -70,7 +72,7 @@ export class SavedObjectEdition extends Component< capabilities, notFoundType, serviceRegistry, - id, + http, serviceName, savedObjectsClient, } = this.props; @@ -80,7 +82,7 @@ export class SavedObjectEdition extends Component< string, boolean >; - const canView = canViewInApp(capabilities, type); + const canView = canViewInApp(capabilities, type) && Boolean(object?.meta.inAppUrl?.path); const service = serviceRegistry.get(serviceName)!.service; return ( @@ -91,7 +93,7 @@ export class SavedObjectEdition extends Component< canViewInApp={canView} type={type} onDeleteClick={() => this.delete()} - viewUrl={service.urlFor(id)} + viewUrl={http.basePath.prepend(object?.meta.inAppUrl?.path || '')} /> {notFoundType && ( <> diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx index 758789aa0f47e..2af7c22488c51 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_edition_page.tsx @@ -11,6 +11,7 @@ import { useParams, useLocation } from 'react-router-dom'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; import { CoreStart, ChromeBreadcrumb, ScopedHistory } from 'src/core/public'; +import { RedirectAppLinks } from '../../../kibana_react/public'; import { ISavedObjectsManagementServiceRegistry } from '../services'; import { SavedObjectEdition } from './object_view'; @@ -50,17 +51,20 @@ const SavedObjectsEditionPage = ({ }, [setBreadcrumbs, service]); return ( - + + + ); }; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 50a08d96de951..27d9b5ce83203 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -5247,6 +5247,36 @@ } } }, + "vis_type_table": { + "properties": { + "total": { + "type": "long" + }, + "total_split": { + "type": "long" + }, + "split_columns": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + }, + "split_rows": { + "properties": { + "total": { + "type": "long" + }, + "enabled": { + "type": "long" + } + } + } + } + }, "vis_type_vega": { "properties": { "vega_lib_specs_total": { diff --git a/src/plugins/vis_type_table/common/index.ts b/src/plugins/vis_type_table/common/index.ts new file mode 100644 index 0000000000000..cc54db82d37e7 --- /dev/null +++ b/src/plugins/vis_type_table/common/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export * from './types'; diff --git a/src/plugins/vis_type_table/common/types.ts b/src/plugins/vis_type_table/common/types.ts new file mode 100644 index 0000000000000..3380e730770c3 --- /dev/null +++ b/src/plugins/vis_type_table/common/types.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export const VIS_TYPE_TABLE = 'table'; + +export enum AggTypes { + SUM = 'sum', + AVG = 'avg', + MIN = 'min', + MAX = 'max', + COUNT = 'count', +} + +export interface TableVisParams { + perPage: number | ''; + showPartialRows: boolean; + showMetricsAtAllLevels: boolean; + showToolbar: boolean; + showTotal: boolean; + totalFunc: AggTypes; + percentageCol: string; + row?: boolean; +} diff --git a/src/plugins/vis_type_table/jest.config.js b/src/plugins/vis_type_table/jest.config.js index 4e5ddbcf8d7c5..3a7906f6ec543 100644 --- a/src/plugins/vis_type_table/jest.config.js +++ b/src/plugins/vis_type_table/jest.config.js @@ -11,4 +11,5 @@ module.exports = { rootDir: '../../..', roots: ['/src/plugins/vis_type_table'], testRunner: 'jasmine2', + collectCoverageFrom: ['/src/plugins/vis_type_table/**/*.{js,ts,tsx}'], }; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options.tsx b/src/plugins/vis_type_table/public/components/table_vis_options.tsx index eb76659a601d6..a70ecb43f1be7 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options.tsx @@ -19,7 +19,7 @@ import { NumberInputOption, VisOptionsProps, } from '../../../vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; import { totalAggregations } from './utils'; const { tabifyGetColumns } = search; diff --git a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx index fb0044a986f5e..716b77e9c91d2 100644 --- a/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx +++ b/src/plugins/vis_type_table/public/components/table_vis_options_lazy.tsx @@ -9,7 +9,7 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { TableVisParams } from '../types'; +import { TableVisParams } from '../../common'; const TableOptionsComponent = lazy(() => import('./table_vis_options')); diff --git a/src/plugins/vis_type_table/public/components/utils.ts b/src/plugins/vis_type_table/public/components/utils.ts index f11d7bc4b7f33..8f30788c76468 100644 --- a/src/plugins/vis_type_table/public/components/utils.ts +++ b/src/plugins/vis_type_table/public/components/utils.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import { AggTypes } from '../types'; +import { AggTypes } from '../../common'; const totalAggregations = [ { diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts index cec16eefb360c..db0b92154d2dd 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition, Datatable, Render } from 'src/plugins/expressions/public'; import { tableVisLegacyResponseHandler, TableContext } from './table_vis_legacy_response_handler'; import { TableVisConfig } from '../types'; +import { VIS_TYPE_TABLE } from '../../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -53,7 +54,7 @@ export const createTableVisLegacyFn = (): TableExpressionFunctionDefinition => ( as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts index a1ceee8c741d4..3e1140275593d 100644 --- a/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts +++ b/src/plugins/vis_type_table/public/legacy/table_vis_legacy_type.ts @@ -12,11 +12,11 @@ import { BaseVisTypeOptions } from '../../../visualizations/public'; import { TableOptions } from '../components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; import { toExpressionAst } from '../to_ast'; -import { TableVisParams } from '../types'; export const tableVisLegacyTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/table_vis_fn.ts b/src/plugins/vis_type_table/public/table_vis_fn.ts index a45f1e828fc47..99fee424b8bea 100644 --- a/src/plugins/vis_type_table/public/table_vis_fn.ts +++ b/src/plugins/vis_type_table/public/table_vis_fn.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { tableVisResponseHandler, TableContext } from './table_vis_response_handler'; import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; import { TableVisConfig } from './types'; +import { VIS_TYPE_TABLE } from '../common'; export type Input = Datatable; @@ -19,7 +20,7 @@ interface Arguments { export interface TableVisRenderValue { visData: TableContext; - visType: 'table'; + visType: typeof VIS_TYPE_TABLE; visConfig: TableVisConfig; } @@ -56,7 +57,7 @@ export const createTableVisFn = (): TableExpressionFunctionDefinition => ({ as: 'table_vis', value: { visData: convertedData, - visType: 'table', + visType: VIS_TYPE_TABLE, visConfig, }, }; diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index 8cd45b54c6ced..ef6d85db103b3 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -10,13 +10,13 @@ import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { BaseVisTypeOptions } from '../../visualizations/public'; +import { TableVisParams, VIS_TYPE_TABLE } from '../common'; import { TableOptions } from './components/table_vis_options_lazy'; import { VIS_EVENT_TO_TRIGGER } from '../../../plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { TableVisParams } from './types'; export const tableVisTypeDefinition: BaseVisTypeOptions = { - name: 'table', + name: VIS_TYPE_TABLE, title: i18n.translate('visTypeTable.tableVisTitle', { defaultMessage: 'Data table', }), diff --git a/src/plugins/vis_type_table/public/to_ast.test.ts b/src/plugins/vis_type_table/public/to_ast.test.ts index 1ca62475b7af0..f0aed7199a2f2 100644 --- a/src/plugins/vis_type_table/public/to_ast.test.ts +++ b/src/plugins/vis_type_table/public/to_ast.test.ts @@ -8,7 +8,7 @@ import { Vis } from 'src/plugins/visualizations/public'; import { toExpressionAst } from './to_ast'; -import { AggTypes, TableVisParams } from './types'; +import { AggTypes, TableVisParams } from '../common'; const mockSchemas = { metric: [{ accessor: 1, format: { id: 'number' }, params: {}, label: 'Count', aggType: 'count' }], diff --git a/src/plugins/vis_type_table/public/to_ast.ts b/src/plugins/vis_type_table/public/to_ast.ts index 9d9f23d31d802..1cbe9832e4c98 100644 --- a/src/plugins/vis_type_table/public/to_ast.ts +++ b/src/plugins/vis_type_table/public/to_ast.ts @@ -12,8 +12,9 @@ import { } from '../../data/public'; import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TableVisParams } from '../common'; import { TableExpressionFunctionDefinition } from './table_vis_fn'; -import { TableVisConfig, TableVisParams } from './types'; +import { TableVisConfig } from './types'; const buildTableVisConfig = ( schemas: ReturnType, diff --git a/src/plugins/vis_type_table/public/types.ts b/src/plugins/vis_type_table/public/types.ts index 75d48f4f53ac7..03cf8bb3395d6 100644 --- a/src/plugins/vis_type_table/public/types.ts +++ b/src/plugins/vis_type_table/public/types.ts @@ -8,14 +8,7 @@ import { IFieldFormat } from 'src/plugins/data/public'; import { SchemaConfig } from 'src/plugins/visualizations/public'; - -export enum AggTypes { - SUM = 'sum', - AVG = 'avg', - MIN = 'min', - MAX = 'max', - COUNT = 'count', -} +import { TableVisParams } from '../common'; export interface Dimensions { buckets: SchemaConfig[]; @@ -44,16 +37,6 @@ export interface TableVisUseUiStateProps { setColumnsWidth: (column: ColumnWidthData) => void; } -export interface TableVisParams { - perPage: number | ''; - showPartialRows: boolean; - showMetricsAtAllLevels: boolean; - showToolbar: boolean; - showTotal: boolean; - totalFunc: AggTypes; - percentageCol: string; -} - export interface TableVisConfig extends TableVisParams { title: string; dimensions: Dimensions; diff --git a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts index 5398aa908f6eb..3a733e7a9a4dc 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_formatted_columns.ts @@ -9,8 +9,9 @@ import { useMemo } from 'react'; import { chain, findIndex } from 'lodash'; +import { AggTypes } from '../../../common'; import { Table } from '../../table_vis_response_handler'; -import { FormattedColumn, TableVisConfig, AggTypes } from '../../types'; +import { FormattedColumn, TableVisConfig } from '../../types'; import { getFormatService } from '../../services'; import { addPercentageColumn } from '../add_percentage_column'; diff --git a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts index 1573a3c6b7b88..7e55e63f9249c 100644 --- a/src/plugins/vis_type_table/public/utils/use/use_pagination.ts +++ b/src/plugins/vis_type_table/public/utils/use/use_pagination.ts @@ -7,7 +7,7 @@ */ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { TableVisParams } from '../../types'; +import { TableVisParams } from '../../../common'; export const usePagination = (visParams: TableVisParams, rowCount: number) => { const [pagination, setPagination] = useState({ diff --git a/src/plugins/vis_type_table/server/index.ts b/src/plugins/vis_type_table/server/index.ts index 75068c646f501..39618d687168e 100644 --- a/src/plugins/vis_type_table/server/index.ts +++ b/src/plugins/vis_type_table/server/index.ts @@ -6,9 +6,11 @@ * Public License, v 1. */ -import { PluginConfigDescriptor } from 'kibana/server'; +import { CoreSetup, PluginConfigDescriptor } from 'kibana/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { configSchema, ConfigSchema } from '../config'; +import { registerVisTypeTableUsageCollector } from './usage_collector'; export const config: PluginConfigDescriptor = { exposeToBrowser: { @@ -21,6 +23,10 @@ export const config: PluginConfigDescriptor = { }; export const plugin = () => ({ - setup() {}, + setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + if (plugins.usageCollection) { + registerVisTypeTableUsageCollector(plugins.usageCollection); + } + }, start() {}, }); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts new file mode 100644 index 0000000000000..55daa5c64349a --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { getStats } from './get_stats'; + +const mockVisualizations = { + saved_objects: [ + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": true }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }, { "schema": "split", "enabled": false }], "params": { "row": true }}', + }, + }, + { + attributes: { + visState: + '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "split", "enabled": true }], "params": { "row": false }}', + }, + }, + { + attributes: { + visState: '{"type": "table","aggs": [{ "schema": "metric" }, { "schema": "bucket" }]}', + }, + }, + { + attributes: { visState: '{"type": "histogram"}' }, + }, + ], +}; + +describe('vis_type_table getStats', () => { + const mockSoClient = ({ + find: jest.fn().mockResolvedValue(mockVisualizations), + } as unknown) as SavedObjectsClientContract; + + test('Returns stats from saved objects for table vis only', async () => { + const result = await getStats(mockSoClient); + expect(mockSoClient.find).toHaveBeenCalledWith({ + type: 'visualization', + perPage: 10000, + }); + expect(result).toEqual({ + total: 4, + total_split: 3, + split_columns: { + total: 1, + enabled: 1, + }, + split_rows: { + total: 2, + enabled: 1, + }, + }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts new file mode 100644 index 0000000000000..bd3e1d2f089e2 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.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 + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; +import { + SavedVisState, + VisualizationSavedObjectAttributes, +} from 'src/plugins/visualizations/common'; +import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; + +export interface VisTypeTableUsage { + /** + * Total number of table type visualizations + */ + total: number; + /** + * Total number of table visualizations, using "Split table" agg + */ + total_split: number; + /** + * Split table by columns stats + */ + split_columns: { + total: number; + enabled: number; + }; + /** + * Split table by rows stats + */ + split_rows: { + total: number; + enabled: number; + }; +} + +/* + * Parse the response data into telemetry payload + */ +export async function getStats( + soClient: SavedObjectsClientContract | ISavedObjectsRepository +): Promise { + const visualizations = await soClient.find({ + type: 'visualization', + perPage: 10000, + }); + + const tableVisualizations = visualizations.saved_objects + .map>(({ attributes }) => JSON.parse(attributes.visState)) + .filter(({ type }) => type === VIS_TYPE_TABLE); + + const defaultStats = { + total: tableVisualizations.length, + total_split: 0, + split_columns: { + total: 0, + enabled: 0, + }, + split_rows: { + total: 0, + enabled: 0, + }, + }; + + return tableVisualizations.reduce((acc, { aggs, params }) => { + const hasSplitAgg = aggs.find((agg) => agg.schema === 'split'); + + if (hasSplitAgg) { + acc.total_split += 1; + + const isSplitRow = params.row; + const isSplitEnabled = hasSplitAgg.enabled; + + const container = isSplitRow ? acc.split_rows : acc.split_columns; + container.total += 1; + container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled; + } + + return acc; + }, defaultStats); +} diff --git a/src/plugins/vis_type_table/server/usage_collector/index.ts b/src/plugins/vis_type_table/server/usage_collector/index.ts new file mode 100644 index 0000000000000..090ed3077b27c --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +export { registerVisTypeTableUsageCollector } from './register_usage_collector'; diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts new file mode 100644 index 0000000000000..cbf39a4d937a7 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +jest.mock('./get_stats', () => ({ + getStats: jest.fn().mockResolvedValue({ somestat: 1 }), +})); + +import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/usage_collection.mock'; +import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; + +import { registerVisTypeTableUsageCollector } from './register_usage_collector'; +import { getStats } from './get_stats'; + +describe('registerVisTypeTableUsageCollector', () => { + it('Usage collector configs fit the shape', () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); + expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); + expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ + type: 'vis_type_table', + isReady: expect.any(Function), + fetch: expect.any(Function), + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + }); + const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; + expect(usageCollectorConfig.isReady()).toBe(true); + }); + + it('Usage collector config.fetch calls getStats', async () => { + const mockCollectorSet = createUsageCollectionSetupMock(); + registerVisTypeTableUsageCollector(mockCollectorSet); + const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; + const mockCollectorFetchContext = createCollectorFetchContextMock(); + const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); + expect(getStats).toBeCalledTimes(1); + expect(getStats).toBeCalledWith(mockCollectorFetchContext.soClient); + expect(fetchResult).toEqual({ somestat: 1 }); + }); +}); diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts new file mode 100644 index 0000000000000..2ac4ce22a47e4 --- /dev/null +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { getStats, VisTypeTableUsage } from './get_stats'; + +export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) { + const collector = collectorSet.makeUsageCollector({ + type: 'vis_type_table', + isReady: () => true, + schema: { + total: { type: 'long' }, + total_split: { type: 'long' }, + split_columns: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + split_rows: { + total: { type: 'long' }, + enabled: { type: 'long' }, + }, + }, + fetch: ({ soClient }) => getStats(soClient), + }); + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json index bda86d06c0ff7..ccff3c349cf21 100644 --- a/src/plugins/vis_type_table/tsconfig.json +++ b/src/plugins/vis_type_table/tsconfig.json @@ -8,6 +8,7 @@ "declarationMap": true }, "include": [ + "common/**/*", "public/**/*", "server/**/*", "*.ts" diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js index 157523cdf09f4..ee9bed141fe4b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_scale.js @@ -7,7 +7,7 @@ */ import d3 from 'd3'; -import _ from 'lodash'; +import { isNumber, reduce, times } from 'lodash'; import moment from 'moment'; import { InvalidLogScaleValues } from '../../errors'; @@ -62,7 +62,7 @@ export class AxisScale { return d3[extent]( opts.reduce(function (opts, v) { - if (!_.isNumber(v)) v = +v; + if (!isNumber(v)) v = +v; if (!isNaN(v)) opts.push(v); return opts; }, []) @@ -90,7 +90,7 @@ export class AxisScale { const y = moment(x); const method = n > 0 ? 'add' : 'subtract'; - _.times(Math.abs(n), function () { + times(Math.abs(n), function () { y[method](interval); }); @@ -100,7 +100,7 @@ export class AxisScale { getAllPoints() { const config = this.axisConfig; const data = this.visConfig.data.chartData(); - const chartPoints = _.reduce( + const chartPoints = reduce( data, (chartPoints, chart, chartIndex) => { const points = chart.series.reduce((points, seri, seriIndex) => { @@ -254,6 +254,6 @@ export class AxisScale { } validateScale(scale) { - if (!scale || _.isNaN(scale)) throw new Error('scale is ' + scale); + if (!scale || Number.isNaN(scale)) throw new Error('scale is ' + scale); } } diff --git a/src/plugins/vis_type_xy/public/chart_splitter.tsx b/src/plugins/vis_type_xy/public/chart_splitter.tsx new file mode 100644 index 0000000000000..bf63ac1896bd1 --- /dev/null +++ b/src/plugins/vis_type_xy/public/chart_splitter.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts'; + +interface ChartSplitterProps { + splitColumnAccessor?: Accessor | AccessorFn; + splitRowAccessor?: Accessor | AccessorFn; + sort?: GroupBySort; +} + +const CHART_SPLITTER_ID = '__chart_splitter__'; + +export const ChartSplitter = ({ + splitColumnAccessor, + splitRowAccessor, + sort, +}: ChartSplitterProps) => + splitColumnAccessor || splitRowAccessor ? ( + <> + { + const splitTypeAccessor = splitColumnAccessor || splitRowAccessor; + if (splitTypeAccessor) { + return typeof splitTypeAccessor === 'function' + ? splitTypeAccessor(datum) + : datum[splitTypeAccessor]; + } + return spec.id; + }} + sort={sort || 'dataIndex'} + /> + + + ) : null; diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index 49b2ab483bc55..02c7157d32c27 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -16,19 +16,20 @@ import { XYChartSeriesIdentifier, } from '@elastic/charts'; -import { BUCKET_TYPES } from '../../../data/public'; - import { Aspects } from '../types'; import './_detailed_tooltip.scss'; import { fillEmptyValue } from '../utils/get_series_name_fn'; -import { COMPLEX_SPLIT_ACCESSOR } from '../utils/accessors'; +import { COMPLEX_SPLIT_ACCESSOR, isRangeAggType } from '../utils/accessors'; interface TooltipData { label: string; value: string; } +// TODO: replace when exported from elastic/charts +const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; + const getTooltipData = ( aspects: Aspects, header: TooltipValue | null, @@ -37,10 +38,7 @@ const getTooltipData = ( const data: TooltipData[] = []; if (header) { - const xFormatter = - aspects.x.aggType === BUCKET_TYPES.DATE_RANGE || aspects.x.aggType === BUCKET_TYPES.RANGE - ? null - : aspects.x.formatter; + const xFormatter = isRangeAggType(aspects.x.aggType) ? null : aspects.x.formatter; data.push({ label: aspects.x.title, value: xFormatter ? xFormatter(header.value) : `${header.value}`, @@ -80,6 +78,28 @@ const getTooltipData = ( } }); + if ( + aspects.splitColumn && + valueSeries.smHorizontalAccessorValue !== undefined && + valueSeries.smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitColumn.title, + value: `${valueSeries.smHorizontalAccessorValue}`, + }); + } + + if ( + aspects.splitRow && + valueSeries.smVerticalAccessorValue !== undefined && + valueSeries.smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + ) { + data.push({ + label: aspects.splitRow.title, + value: `${valueSeries.smVerticalAccessorValue}`, + }); + } + return data; }; diff --git a/src/plugins/vis_type_xy/public/components/index.ts b/src/plugins/vis_type_xy/public/components/index.ts index 260c08e0fc4a9..9b2559bafd18e 100644 --- a/src/plugins/vis_type_xy/public/components/index.ts +++ b/src/plugins/vis_type_xy/public/components/index.ts @@ -11,4 +11,3 @@ export { XYEndzones } from './xy_endzones'; export { XYCurrentTime } from './xy_current_time'; export { XYSettings } from './xy_settings'; export { XYThresholdLine } from './xy_threshold_line'; -export { SplitChartWarning } from './split_chart_warning'; diff --git a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx b/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx deleted file mode 100644 index b708590e04479..0000000000000 --- a/src/plugins/vis_type_xy/public/components/split_chart_warning.tsx +++ /dev/null @@ -1,44 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React, { FC } from 'react'; - -import { EuiLink, EuiCallOut } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -import { getDocLinks } from '../services'; - -export const SplitChartWarning: FC = () => { - const advancedSettingsLink = getDocLinks().links.management.visualizationSettings; - - return ( - - - - - ), - }} - /> - - ); -}; diff --git a/src/plugins/vis_type_xy/public/config/get_aspects.ts b/src/plugins/vis_type_xy/public/config/get_aspects.ts index b8da4386806d4..c031d3fa1fb9b 100644 --- a/src/plugins/vis_type_xy/public/config/get_aspects.ts +++ b/src/plugins/vis_type_xy/public/config/get_aspects.ts @@ -29,7 +29,10 @@ export function getEmptyAspect(): Aspect { }, }; } -export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dimensions): Aspects { +export function getAspects( + columns: DatatableColumn[], + { x, y, z, series, splitColumn, splitRow }: Dimensions +): Aspects { const seriesDimensions = Array.isArray(series) || series === undefined ? series : [series]; return { @@ -37,6 +40,8 @@ export function getAspects(columns: DatatableColumn[], { x, y, z, series }: Dime y: getAspectsFromDimension(columns, y) ?? [], z: z && z?.length > 0 ? getAspectsFromDimension(columns, z[0]) : undefined, series: getAspectsFromDimension(columns, seriesDimensions), + splitColumn: splitColumn?.length ? getAspectsFromDimension(columns, splitColumn[0]) : undefined, + splitRow: splitRow?.length ? getAspectsFromDimension(columns, splitRow[0]) : undefined, }; } diff --git a/src/plugins/vis_type_xy/public/types/config.ts b/src/plugins/vis_type_xy/public/types/config.ts index af3d840739f17..9d4660afa1634 100644 --- a/src/plugins/vis_type_xy/public/types/config.ts +++ b/src/plugins/vis_type_xy/public/types/config.ts @@ -43,6 +43,8 @@ export interface Aspects { y: Aspect[]; z?: Aspect; series?: Aspect[]; + splitColumn?: Aspect; + splitRow?: Aspect; } export interface AxisGrid { diff --git a/src/plugins/vis_type_xy/public/utils/accessors.tsx b/src/plugins/vis_type_xy/public/utils/accessors.tsx index d1337251d36aa..e40248ae92e12 100644 --- a/src/plugins/vis_type_xy/public/utils/accessors.tsx +++ b/src/plugins/vis_type_xy/public/utils/accessors.tsx @@ -26,11 +26,15 @@ const getFieldName = (fieldName: string, index?: number) => { return `${fieldName}${indexStr}`; }; +export const isRangeAggType = (type: string | null) => + type === BUCKET_TYPES.DATE_RANGE || type === BUCKET_TYPES.RANGE; + /** * Returns accessor function for complex accessor types * @param aspect + * @param isComplex - forces to be functional/complex accessor */ -export const getComplexAccessor = (fieldName: string) => ( +export const getComplexAccessor = (fieldName: string, isComplex: boolean = false) => ( aspect: Aspect, index?: number ): Accessor | AccessorFn | undefined => { @@ -38,12 +42,7 @@ export const getComplexAccessor = (fieldName: string) => ( return; } - if ( - !( - (aspect.aggType === BUCKET_TYPES.DATE_RANGE || aspect.aggType === BUCKET_TYPES.RANGE) && - aspect.formatter - ) - ) { + if (!((isComplex || isRangeAggType(aspect.aggType)) && aspect.formatter)) { return aspect.accessor; } @@ -51,7 +50,7 @@ export const getComplexAccessor = (fieldName: string) => ( const accessor = aspect.accessor; const fn: AccessorFn = (d) => { const v = d[accessor]; - if (!v) { + if (v === undefined) { return; } const f = formatter(v); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts new file mode 100644 index 0000000000000..393a6ee06cf58 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.mocks.ts @@ -0,0 +1,386 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { VisConfig } from '../types'; + +export const getVisConfig = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3', + column: 1, + title: 'Average products.base_price', + format: { + id: 'number', + }, + aggType: 'avg', + aggId: '3', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getVisConfigPercentiles = (): VisConfig => { + return { + markSizeRatio: 5.3999999999999995, + fittingFunction: 'linear', + detailedTooltip: true, + isTimeChart: true, + showCurrentTime: false, + showValueLabel: false, + enableHistogramMode: true, + tooltip: { + type: 'vertical', + }, + aspects: { + x: { + accessor: 'col-0-2', + column: 0, + title: 'order_date per minute', + format: { + id: 'date', + params: { + pattern: 'HH:mm', + }, + }, + aggType: 'date_histogram', + aggId: '2', + params: { + date: true, + intervalESUnit: 'm', + intervalESValue: 1, + interval: 60000, + format: 'HH:mm', + }, + }, + y: [ + { + accessor: 'col-1-3.1', + column: 1, + title: '1st percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.1', + params: {}, + }, + { + accessor: 'col-2-3.5', + column: 2, + title: '5th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.5', + params: {}, + }, + { + accessor: 'col-3-3.25', + column: 3, + title: '25th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.25', + params: {}, + }, + { + accessor: 'col-4-3.50', + column: 4, + title: '50th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.50', + params: {}, + }, + { + accessor: 'col-5-3.75', + column: 5, + title: '75th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.75', + params: {}, + }, + { + accessor: 'col-6-3.95', + column: 6, + title: '95th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.95', + params: {}, + }, + { + accessor: 'col-7-3.99', + column: 7, + title: '99th percentile of products.base_price', + format: { + id: 'number', + }, + aggType: 'percentiles', + aggId: '3.99', + params: {}, + }, + ], + }, + xAxis: { + id: 'CategoryAxis-1', + position: 'bottom', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'CategoryAxis-1', + title: 'order_date per minute', + ticks: { + show: true, + showOverlappingLabels: false, + showDuplicates: false, + }, + grid: { + show: false, + }, + scale: { + type: 'time', + }, + integersOnly: false, + }, + yAxes: [ + { + id: 'ValueAxis-1', + position: 'left', + show: true, + style: { + axisTitle: { + visible: true, + }, + tickLabel: { + visible: true, + rotation: 0, + }, + }, + groupId: 'ValueAxis-1', + title: 'Percentiles of products.base_price', + ticks: { + show: true, + rotation: 0, + showOverlappingLabels: true, + showDuplicates: true, + }, + grid: { + show: false, + }, + scale: { + mode: 'normal', + type: 'linear', + }, + domain: {}, + integersOnly: false, + }, + ], + legend: { + show: true, + position: 'right', + }, + rotation: 0, + thresholdLine: { + color: '#E7664C', + show: false, + value: 10, + width: 1, + groupId: 'ValueAxis-1', + }, + }; +}; + +export const getPercentilesData = () => { + return [ + { + 'col-0-2': 1610961900000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 38.49609375, + 'col-5-3.75': 65, + 'col-6-3.95': 65, + 'col-7-3.99': 65, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3.1': 28.984375000000004, + 'col-2-3.5': 28.984375, + 'col-3-3.25': 28.984375, + 'col-4-3.50': 30.9921875, + 'col-5-3.75': 41.5, + 'col-6-3.95': 50, + 'col-7-3.99': 50, + }, + { + 'col-0-2': 1610963280000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 12.9921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + { + 'col-0-2': 1610964180000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 14.9921875, + 'col-4-3.50': 15.98828125, + 'col-5-3.75': 24.984375, + 'col-6-3.95': 85, + 'col-7-3.99': 85, + }, + { + 'col-0-2': 1610964420000, + 'col-1-3.1': 11.9921875, + 'col-2-3.5': 11.9921875, + 'col-3-3.25': 11.9921875, + 'col-4-3.50': 23.99609375, + 'col-5-3.75': 42, + 'col-6-3.95': 42, + 'col-7-3.99': 42, + }, + { + 'col-0-2': 1610964600000, + 'col-1-3.1': 10.9921875, + 'col-2-3.5': 10.992187500000002, + 'col-3-3.25': 10.9921875, + 'col-4-3.50': 12.4921875, + 'col-5-3.75': 13.9921875, + 'col-6-3.95': 13.9921875, + 'col-7-3.99': 13.9921875, + }, + ]; +}; diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx new file mode 100644 index 0000000000000..d76ea49a2f110 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.test.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { AreaSeries, BarSeries, CurveType } from '@elastic/charts'; +import { DatatableRow } from '../../../expressions/public'; +import { renderAllSeries } from './render_all_series'; +import { + getVisConfig, + getVisConfigPercentiles, + getPercentilesData, +} from './render_all_series.test.mocks'; +import { SeriesParam, VisConfig } from '../types'; + +const defaultSeriesParams = [ + { + data: { + id: '3', + label: 'Label', + }, + drawLinesBetweenPoints: true, + interpolate: 'linear', + lineWidth: 2, + mode: 'stacked', + show: true, + showCircles: true, + type: 'area', + valueAxis: 'ValueAxis-1', + }, +] as SeriesParam[]; + +const defaultData = [ + { + 'col-0-2': 1610960220000, + 'col-1-3': 26.984375, + }, + { + 'col-0-2': 1610961300000, + 'col-1-3': 30.99609375, + }, + { + 'col-0-2': 1610961900000, + 'col-1-3': 38.49609375, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3': 35.2421875, + }, +]; + +describe('renderAllSeries', function () { + const getAllSeries = (visConfig: VisConfig, params: SeriesParam[], data: DatatableRow[]) => { + return renderAllSeries( + visConfig, + params, + data, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + }; + + it('renders an area Series and not a bar series if type is area', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(1); + expect(wrapper.find(BarSeries).length).toBe(0); + }); + + it('renders a bar Series in case of histogram', () => { + const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram' }]; + + const renderBarSeries = renderAllSeries( + getVisConfig(), + barSeriesParams as SeriesParam[], + defaultData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderBarSeries}
); + expect(wrapper.find(AreaSeries).length).toBe(0); + expect(wrapper.find(BarSeries).length).toBe(1); + }); + + it('renders the correct yAccessors for not percentile aggs', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual(['col-1-3']); + }); + + it('renders the correct yAccessors for percentile aggs', () => { + const percentilesConfig = getVisConfigPercentiles(); + const percentilesData = getPercentilesData(); + const renderPercentileSeries = renderAllSeries( + percentilesConfig, + defaultSeriesParams as SeriesParam[], + percentilesData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderPercentileSeries}
); + expect(wrapper.find(AreaSeries).prop('yAccessors')).toEqual([ + 'col-1-3.1', + 'col-2-3.5', + 'col-3-3.25', + 'col-4-3.50', + 'col-5-3.75', + 'col-6-3.95', + 'col-7-3.99', + ]); + }); + + it('defaults the CurveType to linear', () => { + const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); + const wrapper = shallow(
{renderSeries}
); + expect(wrapper.find(AreaSeries).prop('curve')).toEqual(CurveType.LINEAR); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx index 264fa539c1980..fb884bb235971 100644 --- a/src/plugins/vis_type_xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_type_xy/public/utils/render_all_series.tsx @@ -71,13 +71,15 @@ export const renderAllSeries = ( interpolate, type, }) => { - const yAspect = aspects.y.find(({ aggId }) => aggId === paramId); - - if (!show || !yAspect || yAspect.accessor === null) { + const yAspects = aspects.y.filter( + ({ aggId, accessor }) => aggId?.includes(paramId) && accessor !== null + ); + if (!show || !yAspects.length) { return null; } + const yAccessors = yAspects.map((aspect) => aspect.accessor) as string[]; - const id = `${type}-${yAspect.accessor}`; + const id = `${type}-${yAccessors[0]}`; const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; const isStacked = mode === 'stacked' || yAxisScale?.mode === 'percentage'; const stackMode = yAxisScale?.mode === 'normal' ? undefined : yAxisScale?.mode; @@ -94,13 +96,13 @@ export const renderAllSeries = ( id={id} name={getSeriesName} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} groupId={pseudoGroupId} useDefaultGroupDomain={useDefaultGroupDomain} xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} splitSeriesAccessors={splitSeriesAccessors} data={data} timeZone={timeZone} @@ -125,7 +127,7 @@ export const renderAllSeries = ( id={id} fit={fittingFunction} color={getSeriesColor} - tickFormat={yAspect.formatter} + tickFormat={yAspects[0].formatter} name={getSeriesName} curve={getCurveType(interpolate)} groupId={pseudoGroupId} @@ -133,7 +135,7 @@ export const renderAllSeries = ( xScaleType={xAxis.scale.type} yScaleType={yAxisScale?.type} xAccessor={xAccessor} - yAccessors={[yAspect.accessor]} + yAccessors={yAccessors} markSizeAccessor={markSizeAccessor} markFormat={aspects.z?.formatter} splitSeriesAccessors={splitSeriesAccessors} diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 0cdabd2fa409e..871fb408d4da0 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -65,6 +65,7 @@ import { getComplexAccessor, getSplitSeriesAccessorFnMap, } from './utils/accessors'; +import { ChartSplitter } from './chart_splitter'; export interface VisComponentProps { visParams: VisParams; @@ -117,7 +118,8 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ): ElementClickListener => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (elements) => { @@ -125,7 +127,8 @@ const VisComponent = (props: VisComponentProps) => { const event = getFilterFromChartClickEventFn( visData, xAccessor, - splitSeriesAccessorFnMap + splitSeriesAccessorFnMap, + splitChartAccessor )(elements as XYChartElementEvent[]); props.fireEvent(event); } @@ -154,12 +157,17 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array + splitSeriesAccessors: Array, + splitChartAccessor?: Accessor | AccessorFn ) => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (series: XYChartSeriesIdentifier): ClickTriggerEvent | null => { if (xAccessor !== null) { - return getFilterFromSeriesFn(visData)(series, splitSeriesAccessorFnMap); + return getFilterFromSeriesFn(visData)( + series, + splitSeriesAccessorFnMap, + splitChartAccessor + ); } return null; @@ -296,10 +304,44 @@ const VisComponent = (props: VisComponentProps) => { ] ); const xAccessor = getXAccessor(config.aspects.x); - const splitSeriesAccessors = config.aspects.series - ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) - : []; + const splitSeriesAccessors = useMemo( + () => + config.aspects.series + ? compact(config.aspects.series.map(getComplexAccessor(COMPLEX_SPLIT_ACCESSOR))) + : [], + [config.aspects.series] + ); + const splitChartColumnAccessor = config.aspects.splitColumn + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitColumn) + : undefined; + const splitChartRowAccessor = config.aspects.splitRow + ? getComplexAccessor(COMPLEX_SPLIT_ACCESSOR, true)(config.aspects.splitRow) + : undefined; + + const renderSeries = useMemo( + () => + renderAllSeries( + config, + visParams.seriesParams, + visData.rows, + getSeriesName, + getSeriesColor, + timeZone, + xAccessor, + splitSeriesAccessors + ), + [ + config, + getSeriesColor, + getSeriesName, + splitSeriesAccessors, + timeZone, + visData.rows, + visParams.seriesParams, + xAccessor, + ] + ); return (
{ legendPosition={legendPosition} /> + { xDomain={xDomain} adjustedXDomain={adjustedXDomain} legendColorPicker={useColorPicker(legendPosition, setColor, getSeriesName)} - onElementClick={handleFilterClick(visData, xAccessor, splitSeriesAccessors)} + onElementClick={handleFilterClick( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + )} onBrushEnd={handleBrush(visData, xAccessor, 'interval' in config.aspects.x.params)} onRenderChange={onRenderChange} legendAction={ config.aspects.series && (config.aspects.series?.length ?? 0) > 0 ? getLegendActions( canFilter, - getFilterEventData(visData, xAccessor, splitSeriesAccessors), + getFilterEventData( + visData, + xAccessor, + splitSeriesAccessors, + splitChartColumnAccessor ?? splitChartRowAccessor + ), handleFilterAction, getSeriesName ) @@ -343,16 +399,7 @@ const VisComponent = (props: VisComponentProps) => { {config.yAxes.map((axisProps) => ( ))} - {renderAllSeries( - config, - visParams.seriesParams, - visData.rows, - getSeriesName, - getSeriesColor, - timeZone, - xAccessor, - splitSeriesAccessors - )} + {renderSeries}
); diff --git a/src/plugins/vis_type_xy/public/vis_renderer.tsx b/src/plugins/vis_type_xy/public/vis_renderer.tsx index 612388939d26b..1a47742b3d004 100644 --- a/src/plugins/vis_type_xy/public/vis_renderer.tsx +++ b/src/plugins/vis_type_xy/public/vis_renderer.tsx @@ -16,7 +16,6 @@ import { VisualizationContainer } from '../../visualizations/public'; import type { PersistedState } from '../../visualizations/public'; import { XyVisType } from '../common'; -import { SplitChartWarning } from './components/split_chart_warning'; import { VisComponentType } from './vis_component'; import { RenderValue, visName } from './xy_vis_fn'; @@ -36,24 +35,20 @@ export const xyVisRenderer: ExpressionRenderDefinition = { reuseDomNode: true, render: async (domNode, { visData, visConfig, visType, syncColors }, handlers) => { const showNoResult = shouldShowNoResultsMessage(visData, visType); - const isSplitChart = Boolean(visConfig.dimensions.splitRow); handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( - <> - {isSplitChart && } - - - - + + + , domNode ); diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/area.tsx rename to src/plugins/vis_type_xy/public/vis_types/area.ts index 50721c349d6e9..09007a01ca8bc 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/area.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getAreaVisTypeDefinition = ( showElasticChartsOptions = false @@ -181,12 +178,6 @@ export const getAreaVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/histogram.tsx rename to src/plugins/vis_type_xy/public/vis_types/histogram.ts index 4fc8dbbb80e7b..daae5f5e48e61 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHistogramVisTypeDefinition = ( showElasticChartsOptions = false @@ -184,12 +181,6 @@ export const getHistogramVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx rename to src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts index b53bb7bc9dd40..9e026fa0d7474 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; import { defaultCountLabel, LabelRotation } from '../../../charts/public'; -import { SplitTooltip } from './split_tooltip'; export const getHorizontalBarVisTypeDefinition = ( showElasticChartsOptions = false @@ -183,12 +180,6 @@ export const getHorizontalBarVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.ts similarity index 94% rename from src/plugins/vis_type_xy/public/vis_types/line.tsx rename to src/plugins/vis_type_xy/public/vis_types/line.ts index e9b0533b957f5..3f3087207fa19 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/line.ts @@ -6,8 +6,6 @@ * Public License, v 1. */ -import React from 'react'; - import { i18n } from '@kbn/i18n'; // @ts-ignore import { euiPaletteColorBlind } from '@elastic/eui/lib/services'; @@ -30,7 +28,6 @@ import { toExpressionAst } from '../to_ast'; import { ChartType } from '../../common'; import { getConfigCollections } from '../editor/collections'; import { getOptionTabs } from '../editor/common_config'; -import { SplitTooltip } from './split_tooltip'; export const getLineVisTypeDefinition = ( showElasticChartsOptions = false @@ -175,12 +172,6 @@ export const getLineVisTypeDefinition = ( min: 0, max: 1, aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'], - // TODO: Remove when split chart aggs are supported - // https://github.com/elastic/kibana/issues/82496 - ...(showElasticChartsOptions && { - disabled: true, - tooltip: , - }), }, ], }, diff --git a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx b/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx deleted file mode 100644 index ca22136599341..0000000000000 --- a/src/plugins/vis_type_xy/public/vis_types/split_tooltip.tsx +++ /dev/null @@ -1,20 +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 - * and the Server Side Public License, v 1; you may not use this file except in - * compliance with, at your election, the Elastic License or the Server Side - * Public License, v 1. - */ - -import React from 'react'; - -import { FormattedMessage } from '@kbn/i18n/react'; - -export function SplitTooltip() { - return ( - - ); -} diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index bd7957164fd1a..fa3dddfeca02a 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -24,8 +24,7 @@ export const uiSettingsConfig: Record> = { description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', { - defaultMessage: - 'Enables legacy charts library for area, line and bar charts in visualize. Currently, only legacy charts library supports split chart aggregation.', + defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.', } ), category: ['visualization'], diff --git a/src/plugins/visualizations/common/index.ts b/src/plugins/visualizations/common/index.ts new file mode 100644 index 0000000000000..d4133eb9b7163 --- /dev/null +++ b/src/plugins/visualizations/common/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +/** @public types */ +export * from './types'; diff --git a/src/plugins/visualizations/common/types.ts b/src/plugins/visualizations/common/types.ts new file mode 100644 index 0000000000000..4881b82a0e8d3 --- /dev/null +++ b/src/plugins/visualizations/common/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { SavedObjectAttributes } from 'kibana/server'; +import { AggConfigOptions } from 'src/plugins/data/common'; + +export interface VisParams { + [key: string]: any; +} + +export interface SavedVisState { + title: string; + type: string; + params: TVisParams; + aggs: AggConfigOptions[]; +} + +export interface VisualizationSavedObjectAttributes extends SavedObjectAttributes { + description: string; + kibanaSavedObjectMeta: { + searchSourceJSON: string; + }; + title: string; + version: number; + visState: string; + uiStateJSON: string; +} diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index d1976cc84acec..0bf8aa6e5c418 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -34,7 +34,7 @@ export type { Schema, ISchemas, } from './vis_types'; -export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; +export { SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; @@ -46,12 +46,13 @@ export { PersistedState } from './persisted_state'; export { VisualizationControllerConstructor, VisualizationController, - SavedVisState, ISavedVis, VisSavedObject, VisResponseValue, VisToExpressionAst, + VisParams, } from './types'; export { ExprVisAPIEvents } from './expressions/vis'; export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; +export { SavedVisState } from '../common'; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts index f3643ad6adcbe..0d871b3b1dea4 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.d.ts +++ b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts @@ -6,7 +6,7 @@ * Public License, v 1. */ -import { SavedVisState } from '../types'; +import { SavedVisState } from '../../common'; declare function updateOldState(oldState: unknown): SavedVisState; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts index c858306968ad8..a85a1d453a939 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references.test.ts @@ -7,7 +7,8 @@ */ import { extractReferences, injectReferences } from './saved_visualization_references'; -import { VisSavedObject, SavedVisState } from '../types'; +import { VisSavedObject } from '../types'; +import { SavedVisState } from '../../common'; describe('extractReferences', () => { test('extracts nothing if savedSearchId is empty', () => { diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index 2e57cd00486f7..dc9ca49840561 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -7,15 +7,12 @@ */ import { SavedObject } from '../../../plugins/saved_objects/public'; -import { - AggConfigOptions, - SearchSourceFields, - TimefilterContract, -} from '../../../plugins/data/public'; +import { SearchSourceFields, TimefilterContract } from '../../../plugins/data/public'; import { ExpressionAstExpression } from '../../expressions/public'; -import { SerializedVis, Vis, VisParams } from './vis'; +import { SerializedVis, Vis } from './vis'; import { ExprVis } from './expressions/vis'; +import { SavedVisState, VisParams } from '../common/types'; export { Vis, SerializedVis, VisParams }; @@ -30,13 +27,6 @@ export type VisualizationControllerConstructor = new ( vis: ExprVis ) => VisualizationController; -export interface SavedVisState { - title: string; - type: string; - params: VisParams; - aggs: AggConfigOptions[]; -} - export interface ISavedVis { id?: string; title: string; diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 58bcdb9ea49c6..56a151fb82ed3 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -30,6 +30,7 @@ import { AggConfigOptions, SearchSourceFields, } from '../../../plugins/data/public'; +import { VisParams } from '../common/types'; export interface SerializedVisData { expression?: string; @@ -56,10 +57,6 @@ export interface VisData { savedSearchId?: string; } -export interface VisParams { - [key: string]: any; -} - const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: string) => { const searchSource = inputSearchSource.createCopy(); if (savedSearchId) { diff --git a/test/examples/config.js b/test/examples/config.js index a720899a637de..aab71cb305016 100644 --- a/test/examples/config.js +++ b/test/examples/config.js @@ -19,6 +19,7 @@ export default async function ({ readConfigFile }) { require.resolve('./ui_actions'), require.resolve('./state_sync'), require.resolve('./routing'), + require.resolve('./expressions_explorer'), ], services: { ...functionalConfig.get('services'), diff --git a/test/examples/expressions_explorer/expressions.ts b/test/examples/expressions_explorer/expressions.ts new file mode 100644 index 0000000000000..7261564e6db38 --- /dev/null +++ b/test/examples/expressions_explorer/expressions.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: PluginFunctionalProviderContext) { + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); + + describe('', () => { + it('runs expression', async () => { + await retry.try(async () => { + const text = await testSubjects.getVisibleText('expressionResult'); + expect(text).to.be( + '{\n "type": "error",\n "error": {\n "message": "Function markdown could not be found.",\n "name": "fn not found"\n }\n}' + ); + }); + }); + + it('renders expression', async () => { + await retry.try(async () => { + const text = await testSubjects.getVisibleText('expressionRender'); + expect(text).to.be('Function markdown could not be found.'); + }); + }); + + it('emits an action and navigates', async () => { + await testSubjects.click('testExpressionButton'); + await retry.try(async () => { + const text = await browser.getCurrentUrl(); + expect(text).to.be('https://www.google.com/?gws_rd=ssl'); + }); + }); + }); +} diff --git a/test/examples/expressions_explorer/index.ts b/test/examples/expressions_explorer/index.ts new file mode 100644 index 0000000000000..77d2a594c0f29 --- /dev/null +++ b/test/examples/expressions_explorer/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * and the Server Side Public License, v 1; you may not use this file except in + * compliance with, at your election, the Elastic License or the Server Side + * Public License, v 1. + */ + +import { PluginFunctionalProviderContext } from 'test/plugin_functional/services'; + +// eslint-disable-next-line import/no-default-export +export default function ({ + getService, + getPageObjects, + loadTestFile, +}: PluginFunctionalProviderContext) { + const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'header']); + + describe('expressions explorer', function () { + before(async () => { + await browser.setWindowSize(1300, 900); + await PageObjects.common.navigateToApp('expressionsExplorer'); + }); + + loadTestFile(require.resolve('./expressions')); + }); +} diff --git a/test/functional/apps/visualize/_inspector.ts b/test/functional/apps/visualize/_inspector.ts index 07b5dfb8a769d..9d4623feef74a 100644 --- a/test/functional/apps/visualize/_inspector.ts +++ b/test/functional/apps/visualize/_inspector.ts @@ -6,12 +6,15 @@ * Public License, v 1. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const inspector = getService('inspector'); const filterBar = getService('filterBar'); + const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { @@ -23,6 +26,34 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + describe('advanced input JSON', () => { + it('should have "missing" property with value 10', async () => { + log.debug('Add Max Metric on memory field'); + await PageObjects.visEditor.clickBucket('Y-axis', 'metrics'); + await PageObjects.visEditor.selectAggregation('Max', 'metrics'); + await PageObjects.visEditor.selectField('memory', 'metrics'); + + log.debug('Add value to advanced JSON input'); + await PageObjects.visEditor.toggleAdvancedParams('2'); + await testSubjects.setValue('codeEditorContainer', '{ "missing": 10 }'); + await PageObjects.visEditor.clickGo(); + + await inspector.open(); + await inspector.openInspectorRequestsView(); + const requestTab = await inspector.getOpenRequestDetailRequestButton(); + await requestTab.click(); + const requestJSON = JSON.parse(await inspector.getCodeEditorValue()); + + expect(requestJSON.aggs['2'].max).property('missing', 10); + }); + + after(async () => { + await inspector.close(); + await PageObjects.visEditor.removeDimension(2); + await PageObjects.visEditor.clickGo(); + }); + }); + describe('inspector table', function indexPatternCreation() { it('should update table header when columns change', async function () { await inspector.open(); diff --git a/test/functional/apps/visualize/_line_chart_split_chart.ts b/test/functional/apps/visualize/_line_chart_split_chart.ts index aeb80a58c9655..3e74bf0b7c0ec 100644 --- a/test/functional/apps/visualize/_line_chart_split_chart.ts +++ b/test/functional/apps/visualize/_line_chart_split_chart.ts @@ -176,8 +176,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -188,8 +188,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabelsAsNumbers(); - const minLabel = 2; - const maxLabel = 5000; + const minLabel = await PageObjects.visChart.getExpectedValue(2, 1); + const maxLabel = await PageObjects.visChart.getExpectedValue(5000, 7000); const numberOfLabels = 10; expect(labels.length).to.be.greaterThan(numberOfLabels); expect(labels[0]).to.eql(minLabel); @@ -201,7 +201,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, false); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -210,7 +213,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -220,7 +226,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); log.debug(labels); - const expectedLabels = ['0', '2,000', '4,000', '6,000', '8,000', '10,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['0', '2,000', '4,000', '6,000', '8,000', '10,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); @@ -228,7 +237,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.visEditor.changeYAxisFilterLabelsCheckbox(axisId, true); await PageObjects.visEditor.clickGo(); const labels = await PageObjects.visChart.getYAxisLabels(); - const expectedLabels = ['2,000', '4,000', '6,000', '8,000']; + const expectedLabels = await PageObjects.visChart.getExpectedValue( + ['2,000', '4,000', '6,000', '8,000'], + ['0', '1,000', '2,000', '3,000', '4,000', '5,000', '6,000', '7,000', '8,000', '9,000'] + ); expect(labels).to.eql(expectedLabels); }); }); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index dddcd82f1d3f8..8dd2854419693 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { // Test replaced vislib chart types loadTestFile(require.resolve('./_area_chart')); loadTestFile(require.resolve('./_line_chart_split_series')); + loadTestFile(require.resolve('./_line_chart_split_chart')); loadTestFile(require.resolve('./_point_series_options')); loadTestFile(require.resolve('./_vertical_bar_chart')); loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 12c3ab12a6998..9472cbf400a6a 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -69,21 +69,26 @@ Table of Contents - [`secrets`](#secrets-6) - [`params`](#params-6) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) - - [`subActionParams (getFields)`](#subactionparams-getfields-1) + - [`subActionParams (getFields)`](#subactionparams-getfields) - [Jira](#jira) - [`config`](#config-7) - [`secrets`](#secrets-7) - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) + - [`subActionParams (getIncident)`](#subactionparams-getincident) - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) - - [`subActionParams (getFields)`](#subactionparams-getfields-2) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype) + - [`subActionParams (issues)`](#subactionparams-issues) + - [`subActionParams (issue)`](#subactionparams-issue) + - [`subActionParams (getFields)`](#subactionparams-getfields-1) - [IBM Resilient](#ibm-resilient) - [`config`](#config-8) - [`secrets`](#secrets-8) - [`params`](#params-8) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - - [`subActionParams (getFields)`](#subactionparams-getfields-3) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (getFields)`](#subactionparams-getfields-2) + - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) + - [`subActionParams (severity)`](#subactionparams-severity) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -526,17 +531,17 @@ The PagerDuty action uses the [V2 Events API](https://v2.developer.pagerduty.com ### `params` -| Property | Description | Type | -| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | -| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | -| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | -| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | -| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | -| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | -| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | -| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | -| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| eventAction | One of `trigger` _(default)_, `resolve`, or `acknowlege`. See [event action](https://v2.developer.pagerduty.com/docs/events-api-v2#event-action) for more details. | string _(optional)_ | +| dedupKey | All actions sharing this key will be associated with the same PagerDuty alert. Used to correlate trigger and resolution. The maximum length is **255** characters. See [alert deduplication](https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication) for details. | string _(optional)_ | +| summary | A text summary of the event, defaults to `No summary provided`. The maximum length is **1024** characters. | string _(optional)_ | +| source | The affected system, preferably a hostname or fully qualified domain name. Defaults to `Kibana Action `. | string _(optional)_ | +| severity | The perceived severity of on the affected system. This can be one of `critical`, `error`, `warning` or `info`_(default)_. | string _(optional)_ | +| timestamp | An [ISO-8601 format date-time](https://v2.developer.pagerduty.com/v2/docs/types#datetime), indicating the time the event was detected or generated. | string _(optional)_ | +| component | The component of the source machine that is responsible for the event, for example `mysql` or `eth0`. | string _(optional)_ | +| group | Logical grouping of components of a service, for example `app-stack`. | string _(optional)_ | +| class | The class/type of the event, for example `ping failure` or `cpu load`. | string _(optional)_ | For more details see [PagerDuty v2 event parameters](https://v2.developer.pagerduty.com/v2/docs/send-an-event-events-api-v2). @@ -550,9 +555,9 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `config` -| Property | Description | Type | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -| apiUrl | ServiceNow instance URL. | string | +| Property | Description | Type | +| -------- | ------------------------ | ------ | +| apiUrl | ServiceNow instance URL. | string | ### `secrets` @@ -563,24 +568,28 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | --------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `getFields`, and `pushToService` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` -| Property | Description | Type | -| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object. | string | -| title | The title of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | -| comment | A comment. | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | -| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | -| severity | The name of the severity in ServiceNow. | string _(optional)_ | -| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | -| impact | The name of the impact in ServiceNow. | string _(optional)_ | +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The ServiceNow incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | +| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| severity | The name of the severity in ServiceNow. | string _(optional)_ | +| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | +| impact | The name of the impact in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -596,9 +605,9 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `config` -| Property | Description | Type | -| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| apiUrl | Jira instance URL. | string | +| Property | Description | Type | +| -------- | ------------------ | ------ | +| apiUrl | Jira instance URL. | string | ### `secrets` @@ -609,48 +618,71 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `params` -| Property | Description | Type | -| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `getIncident`, `issueTypes`, `fieldsByIssueType`, `issues`, `issue`, and `getFields` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object | string | -| title | The title of the issue | string _(optional)_ | -| description | The description of the issue | string _(optional)_ | -| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | -| issueType | The id of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. | string[] _(optional)_ | -| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Jira incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | -#### `subActionParams (issueTypes)` +The following table describes the properties of the `incident` object. -No parameters for `issueTypes` sub-action. Provide an empty object `{}`. +| Property | Description | Type | +| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue | string | +| description | The description of the issue | string _(optional)_ | +| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| issueType | The id of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. | string[] _(optional)_ | +| parent | The parent issue id or key. Only for `Sub-task` issue types. | string _(optional)_ | -#### `subActionParams (getFields)` +#### `subActionParams (getIncident)` -No parameters for `getFields` sub-action. Provide an empty object `{}`. +| Property | Description | Type | +| ---------- | --------------------------- | ------ | +| externalId | The id of the issue in Jira | string | -#### `subActionParams (pushToService)` +#### `subActionParams (issueTypes)` + +No parameters for `issueTypes` sub-action. Provide an empty object `{}`. + +#### `subActionParams (fieldsByIssueType)` | Property | Description | Type | | -------- | -------------------------------- | ------ | | id | The id of the issue type in Jira | string | +#### `subActionParams (issues)` + +| Property | Description | Type | +| -------- | ----------------------- | ------ | +| title | The title to search for | string | + +#### `subActionParams (issue)` + +| Property | Description | Type | +| -------- | --------------------------- | ------ | +| id | The id of the issue in Jira | string | + +#### `subActionParams (getFields)` + +No parameters for `getFields` sub-action. Provide an empty object `{}`. + ## IBM Resilient ID: `.resilient` ### `config` -| Property | Description | Type | -| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | IBM Resilient instance URL. | string | +| Property | Description | Type | +| -------- | --------------------------- | ------ | +| apiUrl | IBM Resilient instance URL. | string | ### `secrets` @@ -661,19 +693,24 @@ ID: `.resilient` ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `getFields`, `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity` | string | +| subActionParams | The parameters of the sub action | object | #### `subActionParams (pushToService)` +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The IBM Resilient incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + | Property | Description | Type | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| savedObjectId | The id of the saved object | string | -| title | The title of the incident | string _(optional)_ | +| name | The title of the incident | string _(optional)_ | | description | The description of the incident | string _(optional)_ | -| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | | externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | | incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | | severityCode | IBM Resilient id of the severity code. | number _(optional)_ | @@ -682,6 +719,14 @@ ID: `.resilient` No parameters for `getFields` sub-action. Provide an empty object `{}`. +#### `subActionParams (incidentTypes)` + +No parameters for `incidentTypes` sub-action. Provide an empty object `{}`. + +#### `subActionParams (severity)` + +No parameters for `severity` sub-action. Provide an empty object `{}`. + # Command Line Utility The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command: diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index ffc2a2ef21fe9..38922fa445a47 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const enableSignificantTerms = 'apm:enableSignificantTerms'; +export const enableCorrelations = 'apm:enableCorrelations'; export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx index b2d88c4c3849b..438303110fbc4 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx @@ -128,7 +128,7 @@ export function LatencyCorrelations() { - View significant terms + View correlations @@ -62,7 +62,7 @@ export function Correlations() { > -

Significant terms

+

Correlations

@@ -88,7 +88,7 @@ export function Correlations() { iconType="alert" >

- Significant terms is an experimental feature and in active + Correlations is an experimental feature and in active development. Bugs and surprises are to be expected but let us know your feedback so we can improve it.

diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx index 392b42cba12e5..29d5750231762 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +17,7 @@ import { I18LABELS } from '../translations'; import { KeyUXMetrics } from './KeyUXMetrics'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useUxQuery } from '../hooks/useUxQuery'; -import { CoreVitals } from '../../../../../../observability/public'; +import { getCoreVitalsComponent } from '../../../../../../observability/public'; import { CsmSharedContext } from '../CsmSharedContext'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { getPercentileLabel } from './translations'; @@ -48,6 +48,18 @@ export function UXMetrics() { sharedData: { totalPageViews }, } = useContext(CsmSharedContext); + const CoreVitals = useMemo( + () => + getCoreVitalsComponent({ + data, + totalPageViews, + loading: status !== 'success', + displayTrafficMetric: true, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [status] + ); + return ( @@ -67,12 +79,7 @@ export function UXMetrics() { - + {CoreVitals} diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index c86fb636b5a1a..e9bb747280fc7 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { - enableSignificantTerms, + enableCorrelations, enableServiceOverview, } from '../common/ui_settings_keys'; @@ -16,17 +16,16 @@ import { * uiSettings definitions for APM. */ export const uiSettings: Record> = { - [enableSignificantTerms]: { + [enableCorrelations]: { category: ['observability'], name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { - defaultMessage: 'APM Significant terms (Platinum required)', + defaultMessage: 'APM correlations (Platinum required)', }), value: false, description: i18n.translate( 'xpack.apm.enableCorrelationsExperimentDescription', { - defaultMessage: - 'Enable the experimental Significant terms feature in APM', + defaultMessage: 'Enable the experimental correlations feature in APM', } ), schema: schema.boolean(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx index b4fbba96e8dfb..341913a033c05 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/debug.tsx @@ -26,9 +26,11 @@ export const debug: RendererFactory = () => ({ ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - handlers.onResize(() => { - ReactDOM.render(renderDebug(), domNode, () => handlers.done()); - }); + if (handlers.onResize) { + handlers.onResize(() => { + ReactDOM.render(renderDebug(), domNode, () => handlers.done()); + }); + } handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); }, diff --git a/x-pack/plugins/case/README.md b/x-pack/plugins/case/README.md index 30011148cd1e7..069441ab640ee 100644 --- a/x-pack/plugins/case/README.md +++ b/x-pack/plugins/case/README.md @@ -4,8 +4,7 @@ Elastic is developing a Case Management Workflow. Follow our progress: -- [Case API Documentation](https://documenter.getpostman.com/view/172706/SW7c2SuF?version=latest) -- [Github Meta](https://github.com/elastic/kibana/issues/50103) +- [Case API Documentation](https://www.elastic.co/guide/en/security/master/cases-overview.html) # Action types @@ -42,27 +41,28 @@ This action type has no `secrets` properties. | description | The case’s description. | string | | tags | String array containing words and phrases that help categorize cases. | string[] | | connector | Object containing the connector’s configuration. | [connector](#connector) | +| settings | Object containing the case’s settings. | [settings](#settings) | #### `subActionParams (update)` -| Property | Description | Type | -| ----------- | ---------------------------------------------------------- | ----------------------- | -| id | The ID of the case being updated. | string | -| tile | The updated case title. | string | -| description | The updated case description. | string | -| tags | The updated case tags. | string | -| connector | Object containing the connector’s configuration. | [connector](#connector) | -| status | The updated case status, which can be: `open` or `closed`. | string | -| version | The current case version. | string | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------- | ----------------------- | +| id | The ID of the case being updated. | string | +| tile | The updated case title. | string | +| description | The updated case description. | string | +| tags | The updated case tags. | string | +| connector | Object containing the connector’s configuration. | [connector](#connector) | +| status | The updated case status, which can be: `open`, `in-progress` or `closed`. | string | +| settings | Object containing the case’s settings. | [settings](#settings) | +| version | The current case version. | string | #### `subActionParams (addComment)` -| Property | Description | Type | -| -------- | ----------------------------------------------------------------------- | ----------------- | -| type | The type of the comment | `user` \| `alert` | -| comment | The comment. Valid only when type is `user`. | string | -| alertId | The alert ID. Valid only when the type is `alert` | string | -| index | The index where the alert is saved. Valid only when the type is `alert` | string | +| Property | Description | Type | +| -------- | ------------------------ | ------ | +| type | The type of the comment. | `user` | +| comment | The comment. | string | + #### `connector` | Property | Description | Type | @@ -96,3 +96,9 @@ For IBM Resilient connectors: | ------------ | ------------------------------- | -------- | | issueTypes | The issue types of the issue. | string[] | | severityCode | The severity code of the issue. | string | + +#### `settings` + +| Property | Description | Type | +| ---------- | ------------------------------ | ------- | +| syncAlerts | Turn on or off alert synching. | boolean | \ No newline at end of file diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 7326d2efb8565..8a4cf1d8566a0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -183,7 +183,7 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen [http.basePath, state.start, state.end, logStreamQuery] ); - const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); + const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); const agentVersion = agent.local_metadata?.elastic?.agent?.version; const isLogFeatureAvailable = useMemo(() => { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 331b6bfa882da..94e81e296b5a9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -115,7 +115,10 @@ async function deleteAssets( try { await Promise.all(deletePromises); } catch (err) { - logger.error(err); + // in the rollback case, partial installs are likely, so missing assets are not an error + if (!savedObjectsClient.errors.isNotFoundError(err)) { + logger.error(err); + } } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index 1ce7b1d85c8e4..0dcdfeb7b3801 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -59,6 +59,7 @@ async function createSetupSideEffects( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentPolicyService.ensureDefaultAgentPolicy(soClient, esClient), + updateFleetRoleIfExists(callCluster), settingsService.getSettings(soClient).catch((e: any) => { if (e.isBoom && e.output.statusCode === 404) { const defaultSettings = createDefaultSettings(); @@ -126,15 +127,25 @@ async function createSetupSideEffects( return { isIntialized: true }; } -export async function setupFleet( - soClient: SavedObjectsClientContract, - esClient: ElasticsearchClient, - callCluster: CallESAsCurrentUser, - options?: { forceRecreate?: boolean } -) { - // Create fleet_enroll role - // This should be done directly in ES at some point - const res = await callCluster('transport.request', { +async function updateFleetRoleIfExists(callCluster: CallESAsCurrentUser) { + try { + await callCluster('transport.request', { + method: 'GET', + path: `/_security/role/${FLEET_ENROLL_ROLE}`, + }); + } catch (e) { + if (e.status === 404) { + return; + } + + throw e; + } + + return putFleetRole(callCluster); +} + +async function putFleetRole(callCluster: CallESAsCurrentUser) { + return callCluster('transport.request', { method: 'PUT', path: `/_security/role/${FLEET_ENROLL_ROLE}`, body: { @@ -156,6 +167,18 @@ export async function setupFleet( ], }, }); +} + +export async function setupFleet( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + callCluster: CallESAsCurrentUser, + options?: { forceRecreate?: boolean } +) { + // Create fleet_enroll role + // This should be done directly in ES at some point + const res = await putFleetRole(callCluster); + // If the role is already created skip the rest unless you have forceRecreate set to true if (options?.forceRecreate !== true && res.role.created === false) { return; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 1cc69ac2239ab..7c32671be93c4 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -70,7 +70,7 @@ export interface MlSetupDependencies { export type MlCoreSetup = CoreSetup; export class MlPlugin implements Plugin { - private appUpdater = new BehaviorSubject(() => ({})); + private appUpdater$ = new BehaviorSubject(() => ({})); private urlGenerator: undefined | UrlGeneratorContract; constructor(private initializerContext: PluginInitializerContext) {} @@ -85,7 +85,7 @@ export class MlPlugin implements Plugin { euiIconType: PLUGIN_ICON_SOLUTION, appRoute: '/app/ml', category: DEFAULT_APP_CATEGORIES.kibana, - updater$: this.appUpdater, + updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); const kibanaVersion = this.initializerContext.env.packageInfo.version; @@ -133,23 +133,34 @@ export class MlPlugin implements Plugin { }); } else { // if ml is disabled in elasticsearch, disable ML in kibana - this.appUpdater.next(() => ({ + this.appUpdater$.next(() => ({ status: AppStatus.inaccessible, })); } // register various ML plugin features which require a full license - const { registerEmbeddables, registerManagementSection, registerMlUiActions } = await import( - './register_helper' - ); - - if (isMlEnabled(license) && isFullLicense(license)) { - const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; - if (canManageMLJobs && pluginsSetup.management !== undefined) { - registerManagementSection(pluginsSetup.management, core).enable(); + // note including registerFeature in register_helper would cause the page bundle size to increase significantly + const { + registerEmbeddables, + registerManagementSection, + registerMlUiActions, + registerSearchLinks, + } = await import('./register_helper'); + + const mlEnabled = isMlEnabled(license); + const fullLicense = isFullLicense(license); + if (mlEnabled) { + registerSearchLinks(this.appUpdater$, fullLicense); + + if (fullLicense) { + const canManageMLJobs = + capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; + if (canManageMLJobs && pluginsSetup.management !== undefined) { + registerManagementSection(pluginsSetup.management, core).enable(); + } + registerEmbeddables(pluginsSetup.embeddable, core); + registerMlUiActions(pluginsSetup.uiActions, core); } - registerEmbeddables(pluginsSetup.embeddable, core); - registerMlUiActions(pluginsSetup.uiActions, core); } }); diff --git a/x-pack/plugins/ml/public/register_helper/index.ts b/x-pack/plugins/ml/public/register_helper/index.ts new file mode 100644 index 0000000000000..8e62b6562520a --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { registerEmbeddables } from '../embeddables'; +export { registerManagementSection } from '../application/management'; +export { registerMlUiActions } from '../ui_actions'; +export { registerSearchLinks } from './register_search_links'; diff --git a/x-pack/plugins/ml/public/register_helper.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts similarity index 51% rename from x-pack/plugins/ml/public/register_helper.ts rename to x-pack/plugins/ml/public/register_helper/register_search_links/index.ts index 50ec53a10ece9..e1912c7ebabeb 100644 --- a/x-pack/plugins/ml/public/register_helper.ts +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/index.ts @@ -4,7 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { registerEmbeddables } from './embeddables'; -export { registerFeature } from './register_feature'; -export { registerManagementSection } from './application/management'; -export { registerMlUiActions } from './ui_actions'; +export { registerSearchLinks } from './register_search_links'; diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts new file mode 100644 index 0000000000000..2df7e8140698a --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/register_search_links.ts @@ -0,0 +1,27 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; + +import { AppUpdater } from 'src/core/public'; +import { getSearchDeepLinks } from './search_deep_links'; + +export function registerSearchLinks( + appUpdater: BehaviorSubject, + isFullLicense: boolean +) { + appUpdater.next(() => ({ + meta: { + keywords: [ + i18n.translate('xpack.ml.keyword.ml', { + defaultMessage: 'ML', + }), + ], + searchDeepLinks: getSearchDeepLinks(isFullLicense), + }, + })); +} diff --git a/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts new file mode 100644 index 0000000000000..7108fb7af5670 --- /dev/null +++ b/x-pack/plugins/ml/public/register_helper/register_search_links/search_deep_links.ts @@ -0,0 +1,110 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import type { AppSearchDeepLink } from 'src/core/public'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; + +const OVERVIEW_LINK_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlOverviewSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.overview', { + defaultMessage: 'Overview', + }), + path: `/${ML_PAGES.OVERVIEW}`, +}; + +const ANOMALY_DETECTION_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlAnomalyDetectionSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.anomalyDetection', { + defaultMessage: 'Anomaly Detection', + }), + path: `/${ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE}`, +}; + +const DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlDataFrameAnalyticsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.dataFrameAnalytics', { + defaultMessage: 'Data Frame Analytics', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`, + searchDeepLinks: [ + { + id: 'mlTrainedModelsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.trainedModels', { + defaultMessage: 'Trained Models', + }), + path: `/${ML_PAGES.DATA_FRAME_ANALYTICS_MODELS_MANAGE}`, + }, + ], +}; + +const DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlDataVisualizerSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.dataVisualizer', { + defaultMessage: 'Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER}`, +}; + +const FILE_UPLOAD_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlFileUploadSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.fileUpload', { + defaultMessage: 'File Upload', + }), + path: `/${ML_PAGES.DATA_VISUALIZER_FILE}`, +}; + +const INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlIndexDataVisualizerSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.indexDataVisualizer', { + defaultMessage: 'Index Data Visualizer', + }), + path: `/${ML_PAGES.DATA_VISUALIZER_INDEX_SELECT}`, +}; + +const SETTINGS_SEARCH_DEEP_LINK: AppSearchDeepLink = { + id: 'mlSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.settings', { + defaultMessage: 'Settings', + }), + path: `/${ML_PAGES.SETTINGS}`, + searchDeepLinks: [ + { + id: 'mlCalendarSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.calendarSettings', { + defaultMessage: 'Calendars', + }), + path: `/${ML_PAGES.CALENDARS_MANAGE}`, + }, + { + id: 'mlFilterListsSettingsSearchDeepLink', + title: i18n.translate('xpack.ml.searchDeepLink.filterListsSettings', { + defaultMessage: 'Filter Lists', + }), + path: `/${ML_PAGES.SETTINGS}`, // Link to settings page as read only users cannot view filter lists. + }, + ], +}; + +export function getSearchDeepLinks(isFullLicense: boolean) { + const deepLinks: AppSearchDeepLink[] = [ + DATA_VISUALIZER_SEARCH_DEEP_LINK, + FILE_UPLOAD_SEARCH_DEEP_LINK, + INDEX_DATA_VISUALIZER_SEARCH_DEEP_LINK, + ]; + + if (isFullLicense === true) { + deepLinks.push( + OVERVIEW_LINK_SEARCH_DEEP_LINK, + ANOMALY_DETECTION_SEARCH_DEEP_LINK, + DATA_FRAME_ANALYTICS_SEARCH_DEEP_LINK, + SETTINGS_SEARCH_DEEP_LINK + ); + } + + return deepLinks; +} diff --git a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts index 3719f4d97779c..42f6eb1f8f9c8 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/anomaly_detectors.ts @@ -5,7 +5,12 @@ */ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; -import { Job } from '../../../common/types/anomaly_detection_jobs'; +import { + Job, + JobStats, + Datafeed, + DatafeedStats, +} from '../../../common/types/anomaly_detection_jobs'; import { GetGuards } from '../shared_services'; export interface AnomalyDetectorsProvider { @@ -14,6 +19,9 @@ export interface AnomalyDetectorsProvider { savedObjectsClient: SavedObjectsClientContract ): { jobs(jobId?: string): Promise<{ count: number; jobs: Job[] }>; + jobStats(jobId?: string): Promise<{ count: number; jobs: JobStats[] }>; + datafeeds(datafeedId?: string): Promise<{ count: number; datafeeds: Datafeed[] }>; + datafeedStats(datafeedId?: string): Promise<{ count: number; datafeeds: DatafeedStats[] }>; }; } @@ -36,6 +44,42 @@ export function getAnomalyDetectorsProvider(getGuards: GetGuards): AnomalyDetect return body; }); }, + async jobStats(jobId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetJobs']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getJobStats<{ + count: number; + jobs: JobStats[]; + }>(jobId !== undefined ? { job_id: jobId } : undefined); + return body; + }); + }, + async datafeeds(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeeds<{ + count: number; + datafeeds: Datafeed[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, + async datafeedStats(datafeedId?: string) { + return await getGuards(request, savedObjectsClient) + .isFullLicense() + .hasMlCapabilities(['canGetDatafeeds']) + .ok(async ({ mlClient }) => { + const { body } = await mlClient.getDatafeedStats<{ + count: number; + datafeeds: DatafeedStats[]; + }>(datafeedId !== undefined ? { datafeed_id: datafeedId } : undefined); + return body; + }); + }, }; }, }; diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index b195bb52e7ed2..097871fe020e5 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n'; import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { usePluginContext } from '../../../hooks/use_plugin_context'; -import { HeaderMenuPortal } from '../../shared/header_menu_portal'; +import HeaderMenuPortal from '../../shared/header_menu_portal'; const Container = styled.div<{ color: string }>` background: ${(props) => props.color}; diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx index 43f1072d06fc2..7074a895d058b 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.tsx @@ -12,7 +12,7 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useHasData } from '../../../../hooks/use_has_data'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { UXHasDataResponse } from '../../../../typings'; -import { CoreVitals } from '../../../shared/core_web_vitals'; +import CoreVitals from '../../../shared/core_web_vitals'; interface Props { bucketSize: string; diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx index f573c8cfc1f97..7d40ce089cec4 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/index.tsx @@ -16,6 +16,7 @@ import { import { CoreVitalItem } from './core_vital_item'; import { WebCoreVitalsTitle } from './web_core_vitals_title'; import { ServiceName } from './service_name'; +import { CoreVitalProps } from '../types'; export interface UXMetrics { cls: number | null; @@ -29,7 +30,7 @@ export interface UXMetrics { clsRanks: number[]; } -export function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string { +function formatToSec(value?: number | string, fromUnit = 'MicroSec'): string { const valueInMs = Number(value ?? 0) / (fromUnit === 'MicroSec' ? 1000 : 1); if (valueInMs < 1000) { @@ -51,23 +52,15 @@ const CoreVitalsThresholds = { CLS: { good: '0.1', bad: '0.25' }, }; -interface Props { - loading: boolean; - data?: UXMetrics | null; - displayServiceName?: boolean; - serviceName?: string; - totalPageViews?: number; - displayTrafficMetric?: boolean; -} - -export function CoreVitals({ +// eslint-disable-next-line import/no-default-export +export default function CoreVitals({ data, loading, displayServiceName, serviceName, totalPageViews, displayTrafficMetric = false, -}: Props) { +}: CoreVitalProps) { const { lcp, lcpRanks, fid, fidRanks, cls, clsRanks, coreVitalPages } = data || {}; return ( diff --git a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx index ca03eb6ddb45a..e209e830d0f37 100644 --- a/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx +++ b/x-pack/plugins/observability/public/components/shared/header_menu_portal.tsx @@ -4,17 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { ReactNode, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; -import { AppMountParameters } from '../../../../../../src/core/public'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderMenuPortalProps } from './types'; -interface HeaderMenuPortalProps { - children: ReactNode; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; -} - -export function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) { +// eslint-disable-next-line import/no-default-export +export default function HeaderMenuPortal({ children, setHeaderActionMenu }: HeaderMenuPortalProps) { const portalNode = useMemo(() => createPortalNode(), []); useEffect(() => { diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx new file mode 100644 index 0000000000000..6e3835129beb2 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -0,0 +1,26 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import { CoreVitalProps, HeaderMenuPortalProps } from './types'; + +export function getCoreVitalsComponent(props: CoreVitalProps) { + const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); + return ( + + + + ); +} + +export function HeaderMenuPortal(props: HeaderMenuPortalProps) { + const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); + return ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/types.ts b/x-pack/plugins/observability/public/components/shared/types.ts new file mode 100644 index 0000000000000..9039f444f550f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { ReactNode } from 'react'; +import { AppMountParameters } from '../../../../../../src/core/public'; +import { UXMetrics } from './core_web_vitals'; + +export interface HeaderMenuPortalProps { + children: ReactNode; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +} + +export interface CoreVitalProps { + loading: boolean; + data?: UXMetrics | null; + displayServiceName?: boolean; + serviceName?: string; + totalPageViews?: number; + displayTrafficMetric?: boolean; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 22cc5faf23967..c052541956c13 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -6,8 +6,7 @@ import { PluginInitializerContext, PluginInitializer } from 'kibana/public'; import { Plugin, ObservabilityPluginSetup, ObservabilityPluginStart } from './plugin'; -export { HeaderMenuPortal } from './components/shared/header_menu_portal'; -export { ObservabilityPluginSetup, ObservabilityPluginStart }; +export type { ObservabilityPluginSetup, ObservabilityPluginStart }; export const plugin: PluginInitializer = ( context: PluginInitializerContext @@ -17,7 +16,8 @@ export const plugin: PluginInitializer { ); }); - it('returns an error message if the name contains invalid characters', () => { - expect(validateTagName('t^ag+name&')).toMatchInlineSnapshot( - `"Tag name can only include a-z, 0-9, _, -,:."` - ); + it('does not return an error message if the name contains special characters', () => { + expect(validateTagName('t^ag+name&')).toBeUndefined(); }); }); diff --git a/x-pack/plugins/saved_objects_tagging/common/validation.ts b/x-pack/plugins/saved_objects_tagging/common/validation.ts index 12149d7bdbe79..5cb9e068516fe 100644 --- a/x-pack/plugins/saved_objects_tagging/common/validation.ts +++ b/x-pack/plugins/saved_objects_tagging/common/validation.ts @@ -12,7 +12,6 @@ export const tagNameMaxLength = 50; export const tagDescriptionMaxLength = 100; const hexColorRegexp = /^#[0-9A-F]{6}$/i; -const nameValidCharsRegexp = /^[0-9A-Z:\-_\s]+$/i; export interface TagValidation { valid: boolean; @@ -49,11 +48,6 @@ export const validateTagName = (name: string): string | undefined => { }, }); } - if (!nameValidCharsRegexp.test(name)) { - return i18n.translate('xpack.savedObjectsTagging.validation.name.errorInvalidCharacters', { - defaultMessage: 'Tag name can only include a-z, 0-9, _, -,:.', - }); - } }; export const validateTagDescription = (description: string): string | undefined => { diff --git a/x-pack/plugins/searchprofiler/server/routes/profile.ts b/x-pack/plugins/searchprofiler/server/routes/profile.ts index 914c688a080f8..87f2ec1df1c92 100644 --- a/x-pack/plugins/searchprofiler/server/routes/profile.ts +++ b/x-pack/plugins/searchprofiler/server/routes/profile.ts @@ -27,10 +27,6 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = }); } - const { - core: { elasticsearch }, - } = ctx; - const { body: { query, index }, } = request; @@ -46,21 +42,25 @@ export const register = ({ router, getLicenseStatus, log }: RouteDependencies) = body: JSON.stringify(parsed, null, 2), }; try { - const resp = await elasticsearch.legacy.client.callAsCurrentUser('search', body); + const client = ctx.core.elasticsearch.client.asCurrentUser; + const resp = await client.search(body); + return response.ok({ body: { ok: true, - resp, + resp: resp.body, }, }); } catch (err) { log.error(err); + const { statusCode, body: errorBody } = err; + return response.customError({ - statusCode: err.status || 500, - body: err.body + statusCode: statusCode || 500, + body: errorBody ? { - message: err.message, - attributes: err.body, + message: errorBody.error?.reason, + attributes: errorBody, } : err, }); diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index f53b5ca6d56ca..c235c296bcbae 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -22,3 +22,16 @@ export const AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER = 'auth_provider_hint'; export const LOGOUT_PROVIDER_QUERY_STRING_PARAMETER = 'provider'; export const LOGOUT_REASON_QUERY_STRING_PARAMETER = 'msg'; export const NEXT_URL_QUERY_STRING_PARAMETER = 'next'; + +/** + * Matches valid usernames and role names. + * + * - Must contain only letters, numbers, spaces, punctuation and printable symbols. + * - Must not contain leading or trailing spaces. + */ +export const NAME_REGEX = /^(?! )[a-zA-Z0-9 !"#$%&'()*+,\-./\\:;<=>?@\[\]^_`{|}~]+(?) { * @param {role} the Role as returned by roles API */ export function isRoleDeprecated(role: Partial) { - return role.metadata?._deprecated ?? false; + return (role.metadata?._deprecated as boolean) ?? false; +} + +/** + * Returns whether given role is a system role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleSystem(role: Partial) { + return (isRoleReserved(role) && role.name?.endsWith('_system')) ?? false; +} + +/** + * Returns whether given role is an admin role or not. + * + * @param {role} the Role as returned by roles API + */ +export function isRoleAdmin(role: Partial) { + return ( + (isRoleReserved(role) && (role.name?.endsWith('_admin') || role.name === 'superuser')) ?? false + ); } /** diff --git a/x-pack/plugins/security/public/components/breadcrumb.tsx b/x-pack/plugins/security/public/components/breadcrumb.tsx new file mode 100644 index 0000000000000..7246e37b33da9 --- /dev/null +++ b/x-pack/plugins/security/public/components/breadcrumb.tsx @@ -0,0 +1,137 @@ +/* + * 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, { createContext, useEffect, useRef, useContext, FunctionComponent } from 'react'; +import { EuiBreadcrumb } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; + +interface BreadcrumbsContext { + parents: BreadcrumbProps[]; + onMount(breadcrumbs: BreadcrumbProps[]): void; + onUnmount(breadcrumbs: BreadcrumbProps[]): void; +} + +const BreadcrumbsContext = createContext(undefined); + +export interface BreadcrumbProps extends EuiBreadcrumb { + text: string; +} + +/** + * Component that automatically sets breadcrumbs and document title based on the render tree. + * + * @example + * // Breadcrumbs will be set to: "Users > Create" + * // Document title will be set to: "Create - Users" + * + * ```typescript + * + * + * {showForm && ( + * + *
+ *
+ * )} + * + * ``` + */ +export const Breadcrumb: FunctionComponent = ({ children, ...breadcrumb }) => { + const context = useContext(BreadcrumbsContext); + const component = {children}; + + if (context) { + return component; + } + + return {component}; +}; + +export interface BreadcrumbsProviderProps { + onChange?: BreadcrumbsChangeHandler; +} + +export type BreadcrumbsChangeHandler = (breadcrumbs: BreadcrumbProps[]) => void; + +/** + * Component that can be used to define any side effects that should occur when breadcrumbs change. + * + * By default the breadcrumbs in application chrome are set and the document title is updated. + * + * @example + * ```typescript + * setBreadcrumbs(breadcrumbs)}> + * + * + * ``` + */ +export const BreadcrumbsProvider: FunctionComponent = ({ + children, + onChange, +}) => { + const { services } = useKibana(); + const breadcrumbsRef = useRef([]); + + const handleChange = (breadcrumbs: BreadcrumbProps[]) => { + if (onChange) { + onChange(breadcrumbs); + } else if (services.chrome) { + services.chrome.setBreadcrumbs(breadcrumbs); + services.chrome.docTitle.change(getDocTitle(breadcrumbs)); + } + }; + + return ( + { + if (breadcrumbs.length > breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + onUnmount: (breadcrumbs) => { + if (breadcrumbs.length < breadcrumbsRef.current.length) { + breadcrumbsRef.current = breadcrumbs; + handleChange(breadcrumbs); + } + }, + }} + > + {children} + + ); +}; + +export interface InnerBreadcrumbProps { + breadcrumb: BreadcrumbProps; +} + +export const InnerBreadcrumb: FunctionComponent = ({ + breadcrumb, + children, +}) => { + const { parents, onMount, onUnmount } = useContext(BreadcrumbsContext)!; + const nextParents = [...parents, breadcrumb]; + + useEffect(() => { + onMount(nextParents); + return () => onUnmount(parents); + }, [breadcrumb.text, breadcrumb.href]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {children} + + ); +}; + +export function getDocTitle(breadcrumbs: BreadcrumbProps[], maxBreadcrumbs = 2) { + return breadcrumbs + .slice(0, maxBreadcrumbs) + .reverse() + .map(({ text }) => text); +} diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx new file mode 100644 index 0000000000000..8dfbf9e3f0649 --- /dev/null +++ b/x-pack/plugins/security/public/components/confirm_modal.tsx @@ -0,0 +1,94 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalProps, + EuiOverlayMask, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ConfirmModalProps extends Omit { + confirmButtonText: string; + confirmButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; + onCancel(): void; + onConfirm(): void; + ownFocus?: boolean; +} + +/** + * Component that renders a confirmation modal similar to `EuiConfirmModal`, except that + * it adds `isLoading` prop, which renders a loading spinner and disables action buttons, + * and `ownFocus` prop to render overlay mask. + */ +export const ConfirmModal: FunctionComponent = ({ + children, + confirmButtonColor: buttonColor, + confirmButtonText, + isLoading, + isDisabled, + onCancel, + onConfirm, + ownFocus = true, + title, + ...rest +}) => { + const modal = ( + + + {title} + + {children} + + + + + + + + + + {confirmButtonText} + + + + + + ); + + return ownFocus ? ( + {modal} + ) : ( + modal + ); +}; diff --git a/x-pack/plugins/security/public/components/doc_link.tsx b/x-pack/plugins/security/public/components/doc_link.tsx new file mode 100644 index 0000000000000..50a93b8ee5090 --- /dev/null +++ b/x-pack/plugins/security/public/components/doc_link.tsx @@ -0,0 +1,60 @@ +/* + * 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, { useCallback, FunctionComponent } from 'react'; +import { EuiLink } from '@elastic/eui'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { CoreStart } from '../../../../../src/core/public'; + +export type DocLinks = CoreStart['docLinks']['links']; +export type GetDocLinkFunction = (app: string, doc: string) => string; + +/** + * Creates links to the documentation. + * + * @see {@link DocLink} for a component that creates a link to the docs. + * + * @example + * ```typescript + * + * Learn what privileges individual roles grant. + * + * ``` + * + * @example + * ```typescript + * const [docs] = useDocLinks(); + * + * + * Learn how to get started with dashboards. + * + * ``` + */ +export function useDocLinks(): [DocLinks, GetDocLinkFunction] { + const { services } = useKibana(); + const { links, ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = services.docLinks!; + const getDocLink = useCallback( + (app, doc) => { + return `${ELASTIC_WEBSITE_URL}guide/en/${app}/reference/${DOC_LINK_VERSION}/${doc}`; + }, + [ELASTIC_WEBSITE_URL, DOC_LINK_VERSION] + ); + return [links, getDocLink]; +} + +export interface DocLinkProps { + app: string; + doc: string; +} + +export const DocLink: FunctionComponent = ({ app, doc, children }) => { + const [, getDocLink] = useDocLinks(); + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/security/public/components/form_flyout.tsx b/x-pack/plugins/security/public/components/form_flyout.tsx new file mode 100644 index 0000000000000..a0d397f81751e --- /dev/null +++ b/x-pack/plugins/security/public/components/form_flyout.tsx @@ -0,0 +1,97 @@ +/* + * 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, { useEffect, FunctionComponent, RefObject } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiTitle, + EuiFlyout, + EuiFlyoutProps, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonProps, + EuiButtonEmpty, + EuiPortal, +} from '@elastic/eui'; +import { useHtmlId } from './use_html_id'; + +export interface FormFlyoutProps extends Omit { + title: string; + initialFocus?: RefObject; + onCancel(): void; + onSubmit(): void; + submitButtonText: string; + submitButtonColor?: EuiButtonProps['color']; + isLoading?: EuiButtonProps['isLoading']; + isDisabled?: EuiButtonProps['isDisabled']; +} + +export const FormFlyout: FunctionComponent = ({ + title, + submitButtonText, + submitButtonColor, + onCancel, + onSubmit, + isLoading, + isDisabled, + children, + initialFocus, + ...rest +}) => { + useEffect(() => { + if (initialFocus && initialFocus.current) { + initialFocus.current.focus(); + } + }, [initialFocus]); + + const titleId = useHtmlId('formFlyout', 'title'); + + return ( + + + + +

{title}

+
+
+ {children} + + + + + + + + + + {submitButtonText} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/components/use_current_user.ts b/x-pack/plugins/security/public/components/use_current_user.ts new file mode 100644 index 0000000000000..b686e0ae9d778 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_current_user.ts @@ -0,0 +1,24 @@ +/* + * 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 useAsync from 'react-use/lib/useAsync'; +import constate from 'constate'; +import { AuthenticationServiceSetup } from '../authentication'; + +export interface AuthenticationProviderProps { + authc: AuthenticationServiceSetup; +} + +const [AuthenticationProvider, useAuthentication] = constate( + ({ authc }: AuthenticationProviderProps) => authc +); + +export { AuthenticationProvider, useAuthentication }; + +export function useCurrentUser() { + const authc = useAuthentication(); + return useAsync(authc.getCurrentUser, [authc]); +} diff --git a/x-pack/plugins/security/public/components/use_form.ts b/x-pack/plugins/security/public/components/use_form.ts new file mode 100644 index 0000000000000..33c7e184ec171 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_form.ts @@ -0,0 +1,205 @@ +/* + * 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 { ChangeEventHandler, FocusEventHandler, ReactEventHandler, useState } from 'react'; +import { get, set, cloneDeep, cloneDeepWith } from 'lodash'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +export type FormReturnTuple = [FormState, FormProps]; + +export interface FormProps { + onSubmit: ReactEventHandler; + onChange: ChangeEventHandler; + onBlur: FocusEventHandler; +} + +export interface FormOptions { + onSubmit: SubmitCallback; + validate: ValidateCallback; + defaultValues: Values; +} + +/** + * Returns state and {@link HTMLFormElement} event handlers useful for creating + * forms with inline validation. + * + * @see {@link useFormState} if you don't want to use {@link HTMLFormElement}. + * + * @example + * ```typescript + * const [form, eventHandlers] = useForm({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.email ? { email: 'Required' } : {} + * }); + * + * + * + * Submit + * + * ``` + */ +export function useForm( + options: FormOptions +): FormReturnTuple { + const form = useFormState(options); + + const eventHandlers: FormProps = { + onSubmit: (event) => { + event.preventDefault(); + form.submit(); + }, + onChange: (event) => { + const { name, type, checked, value } = event.target; + if (name) { + form.setValue(name, type === 'checkbox' ? checked : value); + } + }, + onBlur: (event) => { + const { name } = event.target; + if (name) { + form.setTouched(event.target.name); + } + }, + }; + + return [form, eventHandlers]; +} + +export type FormValues = Record; +export type SubmitCallback = (values: Values) => Promise; +export type ValidateCallback = ( + values: Values +) => ValidationErrors | Promise>; +export type ValidationErrors = DeepMap; +export type TouchedFields = DeepMap; + +export interface FormState { + setValue(name: string, value: any): Promise; + setError(name: string, message: string): void; + setTouched(name: string): Promise; + reset(values: Values): void; + submit(): Promise; + values: Values; + errors: ValidationErrors; + touched: TouchedFields; + isValidating: boolean; + isSubmitting: boolean; + submitError: Error | undefined; + isInvalid: boolean; + isSubmitted: boolean; +} + +/** + * Returns state useful for creating forms with inline validation. + * + * @example + * ```typescript + * const form = useFormState({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.toggle ? { toggle: 'Required' } : {} + * }); + * + * form.setValue('toggle', e.target.checked)} + * onBlur={() => form.setTouched('toggle')} + * isInvalid={!!form.errors.toggle} + * /> + * + * Submit + * + * ``` + */ +export function useFormState({ + onSubmit, + validate, + defaultValues, +}: FormOptions): FormState { + const [values, setValues] = useState(defaultValues); + const [errors, setErrors] = useState>({}); + const [touched, setTouched] = useState>({}); + const [submitCount, setSubmitCount] = useState(0); + + const [validationState, validateForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validate(formValues); + setErrors(nextErrors); + if (Object.keys(nextErrors).length === 0) { + setSubmitCount(0); + } + return nextErrors; + }, + [validate] + ); + + const [submitState, submitForm] = useAsyncFn( + async (formValues: Values) => { + const nextErrors = await validateForm(formValues); + setTouched(mapDeep(formValues, true)); + setSubmitCount(submitCount + 1); + if (Object.keys(nextErrors).length === 0) { + return onSubmit(formValues); + } + }, + [validateForm, onSubmit] + ); + + return { + setValue: async (name, value) => { + const nextValues = setDeep(values, name, value); + setValues(nextValues); + await validateForm(nextValues); + }, + setTouched: async (name, value = true) => { + setTouched(setDeep(touched, name, value)); + await validateForm(values); + }, + setError: (name, message) => { + setErrors(setDeep(errors, name, message)); + setTouched(setDeep(touched, name, true)); + }, + reset: (nextValues) => { + setValues(nextValues); + setErrors({}); + setTouched({}); + setSubmitCount(0); + }, + submit: () => submitForm(values), + values, + errors, + touched, + isValidating: validationState.loading, + isSubmitting: submitState.loading, + submitError: submitState.error, + isInvalid: Object.keys(errors).length > 0, + isSubmitted: submitCount > 0, + }; +} + +type DeepMap = { + [K in keyof T]?: T[K] extends any[] + ? T[K][number] extends object + ? Array> + : TValue + : T[K] extends object + ? DeepMap + : TValue; +}; + +function mapDeep(values: T, value: V): DeepMap { + return cloneDeepWith(values, (v) => { + if (typeof v !== 'object' && v !== null) { + return value; + } + }); +} + +function setDeep(values: T, name: string, value: V): T { + if (get(values, name) !== value) { + return set(cloneDeep(values), name, value); + } + return values; +} diff --git a/x-pack/plugins/security/public/components/use_html_id.ts b/x-pack/plugins/security/public/components/use_html_id.ts new file mode 100644 index 0000000000000..23666e83cbf23 --- /dev/null +++ b/x-pack/plugins/security/public/components/use_html_id.ts @@ -0,0 +1,27 @@ +/* + * 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 { useMemo } from 'react'; +import { htmlIdGenerator } from '@elastic/eui'; + +/** + * Generates an ID that can be used for HTML elements. + * + * @param prefix Prefix of the id to be generated + * @param suffix Suffix of the id to be generated + * + * @example + * ```typescript + * const titleId = useHtmlId('changePasswordForm', 'title'); + * + * + *

Change password

+ *
+ * ``` + */ +export function useHtmlId(prefix?: string, suffix?: string) { + return useMemo(() => htmlIdGenerator(prefix)(suffix), [prefix, suffix]); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx index c5582d3526242..b7808ffb30e74 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.test.tsx @@ -5,141 +5,153 @@ */ import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; - +import { shallowWithIntl } from '@kbn/test/jest'; import { RoleComboBox } from '.'; -import { EuiComboBox } from '@elastic/eui'; -import { findTestSubject } from '@kbn/test/jest'; describe('RoleComboBox', () => { - it('renders the provided list of roles via EuiComboBox options', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "default", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": false, + it('renders roles grouped by custom, user, admin, system and deprecated roles with correct color', () => { + const wrapper = shallowWithIntl( + { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: { _deprecated: true }, - }, - ]; - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(EuiComboBox).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "color": "warning", - "data-test-subj": "roleOption-role-1", - "label": "role-1", - "value": Object { - "isDeprecated": true, + { + name: 'some_admin', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, }, - }, - ] - `); - }); - - it('renders the selected role names in the expanded list, coded according to deprecated status', () => { - const availableRoles = [ - { - name: 'role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - { - name: 'role-2', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: [], - metadata: {}, - }, - ]; - const wrapper = mountWithIntl( -
- -
+ { + name: 'some_system', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: false }, + }, + { + name: 'deprecated_role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + metadata: { _reserved: true, _deprecated: true }, + }, + ]} + selectedRoleNames={[]} + onChange={jest.fn()} + /> ); - findTestSubject(wrapper, 'comboBoxToggleListButton').simulate('click'); - - wrapper.find(EuiComboBox).setState({ isListOpen: true }); - - expect(findTestSubject(wrapper, 'rolesDropdown-renderOption')).toMatchInlineSnapshot(` - Array [ -
- -
- role-1 - -
-
-
, -
- -
- role-2 - -
-
-
, - ] + expect(wrapper).toMatchInlineSnapshot(` + `); }); }); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx index 5b24b296b299f..91d953c4aa29a 100644 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx +++ b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box.tsx @@ -6,11 +6,28 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox } from '@elastic/eui'; -import { Role, isRoleDeprecated } from '../../../common/model'; -import { RoleComboBoxOption } from './role_combo_box_option'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiBadge, + EuiComboBox, + EuiComboBoxProps, + EuiComboBoxOptionOption, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { + Role, + isRoleSystem, + isRoleAdmin, + isRoleReserved, + isRoleDeprecated, +} from '../../../common/model'; -interface Props { +interface Props + extends Omit< + EuiComboBoxProps, + 'onChange' | 'options' | 'selectedOptions' | 'renderOption' + > { availableRoles: Role[]; selectedRoleNames: readonly string[]; onChange: (selectedRoleNames: string[]) => void; @@ -19,43 +36,132 @@ interface Props { isDisabled?: boolean; } +type Option = EuiComboBoxOptionOption<{ + isReserved: boolean; + isDeprecated: boolean; + isSystem: boolean; + isAdmin: boolean; + deprecatedReason?: string; +}>; + export const RoleComboBox = (props: Props) => { const onRolesChange = (selectedItems: Array<{ label: string }>) => { props.onChange(selectedItems.map((item) => item.label)); }; - const roleNameToOption = (roleName: string) => { + const roleNameToOption = (roleName: string): Option => { const roleDefinition = props.availableRoles.find((role) => role.name === roleName); + const isReserved: boolean = (roleDefinition && isRoleReserved(roleDefinition)) ?? false; const isDeprecated: boolean = (roleDefinition && isRoleDeprecated(roleDefinition)) ?? false; + const isSystem: boolean = (roleDefinition && isRoleSystem(roleDefinition)) ?? false; + const isAdmin: boolean = (roleDefinition && isRoleAdmin(roleDefinition)) ?? false; return { - color: isDeprecated ? 'warning' : 'default', + color: isDeprecated ? 'warning' : isReserved ? 'primary' : undefined, 'data-test-subj': `roleOption-${roleName}`, label: roleName, value: { + isReserved, isDeprecated, + isSystem, + isAdmin, + deprecatedReason: roleDefinition?.metadata?._deprecated_reason, }, }; }; const options = props.availableRoles.map((role) => roleNameToOption(role.name)); - const selectedOptions = props.selectedRoleNames.map(roleNameToOption); + const groupedOptions = options.reduce>((acc, option) => { + const type = option.value?.isDeprecated + ? 'deprecated' + : option.value?.isSystem + ? 'system' + : option.value?.isAdmin + ? 'admin' + : option.value?.isReserved + ? 'user' + : 'custom'; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(option); + return acc; + }, {}); return ( } + renderOption={renderOption} /> ); }; + +function renderOption(option: Option) { + return ( + + {option.label} + {option.value?.isDeprecated ? ( + + + + + + ) : option.value?.isReserved ? ( + + + + + + ) : undefined} + + ); +} diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx deleted file mode 100644 index b24a48145b461..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.test.tsx +++ /dev/null @@ -1,57 +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 { shallowWithIntl } from '@kbn/test/jest'; -import { RoleComboBoxOption } from './role_combo_box_option'; - -describe('RoleComboBoxOption', () => { - it('renders a regular role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - - `); - }); - - it('renders a deprecated role correctly', () => { - const wrapper = shallowWithIntl( - - ); - - expect(wrapper).toMatchInlineSnapshot(` - - role-1 - - (deprecated) - - `); - }); -}); diff --git a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx b/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx deleted file mode 100644 index ae9b79c796275..0000000000000 --- a/x-pack/plugins/security/public/management/role_combo_box/role_combo_box_option.tsx +++ /dev/null @@ -1,31 +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 { i18n } from '@kbn/i18n'; - -import { EuiComboBoxOptionOption, EuiText } from '@elastic/eui'; - -interface Props { - option: EuiComboBoxOptionOption<{ isDeprecated: boolean }>; -} - -export const RoleComboBoxOption = ({ option }: Props) => { - const isDeprecated = option.value?.isDeprecated ?? false; - const deprecatedLabel = i18n.translate( - 'xpack.security.management.users.editUser.deprecatedRoleText', - { - defaultMessage: '(deprecated)', - } - ); - - return ( - - {option.label} {isDeprecated ? deprecatedLabel : ''} - - ); -}; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index c750ec373b9f7..df5e5c8be9025 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -407,7 +407,7 @@ export const EditRolePage: FunctionComponent = ({ const onNameChange = (e: ChangeEvent) => setRole({ ...role, - name: e.target.value.replace(/\s/g, '_'), + name: e.target.value, }); const getElasticsearchPrivileges = () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts index 868674aec6f86..e6b9b19022f31 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.test.ts @@ -40,7 +40,7 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Please provide a role name`, + error: `Please provide a role name.`, }); }); @@ -57,13 +57,30 @@ describe('validateRoleName', () => { expect(validator.validateRoleName(role)).toEqual({ isInvalid: true, - error: `Name must not exceed 1024 characters`, + error: `Name must not exceed 1024 characters.`, + }); + }); + + test('it cannot start with whitespace character', () => { + const role = { + name: ' role-name', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + expect(validator.validateRoleName(role)).toEqual({ + isInvalid: true, + error: `Name must not contain leading or trailing spaces.`, }); }); const charList = `!#%^&*()+=[]{}\|';:"/,<>?`.split(''); charList.forEach((element) => { - test(`it cannot support the "${element}" character`, () => { + test(`it allows the "${element}" character`, () => { const role = { name: `role-${element}`, elasticsearch: { @@ -74,10 +91,7 @@ describe('validateRoleName', () => { kibana: [], }; - expect(validator.validateRoleName(role)).toEqual({ - isInvalid: true, - error: `Name must begin with a letter or underscore and contain only letters, underscores, and numbers.`, - }); + expect(validator.validateRoleName(role)).toEqual({ isInvalid: false }); }); }); }); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts index 89b16b1467776..e0459bbd3dd0d 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/validate_role.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { Role, RoleIndexPrivilege } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; interface RoleValidatorOptions { shouldValidate?: boolean; @@ -41,25 +42,36 @@ export class RoleValidator { i18n.translate( 'xpack.security.management.editRole.validateRole.provideRoleNameWarningMessage', { - defaultMessage: 'Please provide a role name', + defaultMessage: 'Please provide a role name.', } ) ); } - if (role.name.length > 1024) { + if (role.name.length > MAX_NAME_LENGTH) { return invalid( i18n.translate('xpack.security.management.editRole.validateRole.nameLengthWarningMessage', { - defaultMessage: 'Name must not exceed 1024 characters', + defaultMessage: 'Name must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, }) ); } - if (!role.name.match(/^[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*$/)) { + if (role.name.trim() !== role.name) { + return invalid( + i18n.translate( + 'xpack.security.management.editRole.validateRole.nameWhitespaceWarningMessage', + { + defaultMessage: `Name must not contain leading or trailing spaces.`, + } + ) + ); + } + if (!role.name.match(NAME_REGEX)) { return invalid( i18n.translate( 'xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage', { defaultMessage: - 'Name must begin with a letter or underscore and contain only letters, underscores, and numbers.', + 'Name must contain only letters, numbers, spaces, punctuation and printable symbols.', } ) ); diff --git a/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx new file mode 100644 index 0000000000000..2586b7c24bf4c --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/change_password_flyout.tsx @@ -0,0 +1,286 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiCallOut, + EuiFieldPassword, + EuiFlexGroup, + EuiForm, + EuiFormRow, + EuiIcon, + EuiLoadingContent, + EuiSpacer, + EuiText, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { useCurrentUser } from '../../../components/use_current_user'; +import { FormFlyout } from '../../../components/form_flyout'; +import { UserAPIClient } from '..'; + +export interface ChangePasswordFormValues { + current_password?: string; + password: string; + confirm_password: string; +} + +export interface ChangePasswordFlyoutProps { + username: string; + defaultValues?: ChangePasswordFormValues; + onCancel(): void; + onSuccess?(): void; +} + +export const ChangePasswordFlyout: FunctionComponent = ({ + username, + defaultValues = { + current_password: '', + password: '', + confirm_password: '', + }, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + const { value: currentUser, loading: isLoading } = useCurrentUser(); + const isCurrentUser = currentUser?.username === username; + const isSystemUser = username === 'kibana' || username === 'kibana_system'; + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + try { + await new UserAPIClient(services.http!).changePassword( + username, + values.password, + values.current_password + ); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.changePasswordFlyout.successMessage', { + defaultMessage: "Password changed for '{username}'.", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + if ((error as any).body?.message === 'security_exception') { + form.setError( + 'current_password', + i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordInvalidError', + { + defaultMessage: 'Invalid password.', + } + ) + ); + } else { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.errorMessage', + { + defaultMessage: 'Could not change password', + } + ), + text: (error as any).body?.message || error.message, + }); + throw error; + } + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isCurrentUser) { + if (!values.current_password) { + errors.current_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.currentPasswordRequiredError', + { + defaultMessage: 'Enter your current password.', + } + ); + } + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordRequiredError', + { + defaultMessage: 'Enter a new password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.changePasswordFlyout.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + + return errors; + }, + defaultValues, + }); + + return ( + + {isLoading ? ( + + ) : ( + + {isSystemUser ? ( + <> + +

+ +

+

+ +

+
+ + + ) : undefined} + + + + + + + + + {username} + + + + + + {isCurrentUser ? ( + + + + ) : null} + + + + + + + + + {/* Hidden submit button is required for enter key to trigger form submission */} + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx new file mode 100644 index 0000000000000..18be46ebefed0 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -0,0 +1,96 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDeleteUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDeleteUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, deleteUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).deleteUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDeleteUsers.successMessage', { + defaultMessage: "Deleted user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmDeleteUsers.errorMessage', { + defaultMessage: "Could not delete user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx new file mode 100644 index 0000000000000..b0a9e875c2089 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -0,0 +1,119 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmDisableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmDisableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + const isSystemUser = usernames[0] === 'kibana' || usernames[0] === 'kibana_system'; + + const [state, disableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).disableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmDisableUsers.successMessage', { + defaultMessage: "Deactivated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate( + 'xpack.security.management.users.confirmDisableUsers.errorMessage', + { + defaultMessage: "Could not deactivate user '{username}'", + values: { username }, + } + ), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + {isSystemUser ? ( + +

+ +

+

+ +

+
+ ) : ( + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx new file mode 100644 index 0000000000000..c9589cfa17da2 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -0,0 +1,89 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ConfirmModal } from '../../../components/confirm_modal'; +import { UserAPIClient } from '..'; + +export interface ConfirmEnableUsersProps { + usernames: string[]; + onCancel(): void; + onSuccess?(): void; +} + +export const ConfirmEnableUsers: FunctionComponent = ({ + usernames, + onCancel, + onSuccess, +}) => { + const { services } = useKibana(); + + const [state, enableUsers] = useAsyncFn(async () => { + for (const username of usernames) { + try { + await new UserAPIClient(services.http!).enableUser(username); + services.notifications!.toasts.addSuccess( + i18n.translate('xpack.security.management.users.confirmEnableUsers.successMessage', { + defaultMessage: "Activated user '{username}'", + values: { username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.confirmEnableUsers.errorMessage', { + defaultMessage: "Could not activate user '{username}'", + values: { username }, + }), + text: (error as any).body?.message || error.message, + }); + } + } + }, [services.http]); + + return ( + + +

+ +

+ {usernames.length > 1 && ( +
    + {usernames.map((username) => ( +
  • {username}
  • + ))} +
+ )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx new file mode 100644 index 0000000000000..e7e3e1164ae14 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.test.tsx @@ -0,0 +1,108 @@ +/* + * 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 { render, fireEvent, waitFor, within } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { securityMock } from '../../../mocks'; +import { Providers } from '../users_management_app'; +import { CreateUserPage } from './create_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('CreateUserPage', () => { + it('creates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + coreStart.http.post.mockResolvedValue({}); + + const { findByRole, findByLabelText } = render( + + + + ); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'jdoe' } }); + fireEvent.change(await findByLabelText('Password'), { target: { value: 'changeme' } }); + fireEvent.change(await findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + password: 'changeme', + username: 'jdoe', + full_name: '', + email: '', + roles: [], + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/create'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.get.mockResolvedValueOnce([ + { + username: 'existing_username', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }, + ]); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Create user' })); + + const alert = await findByRole('alert'); + within(alert).getByText(/Enter a username/i); + within(alert).getByText(/Enter a password/i); + + fireEvent.change(await findByLabelText('Username'), { target: { value: 'existing_username' } }); + + await findAllByText(/User 'existing_username' already exists/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: ' username_with_leading_space' }, + }); + + await findAllByText(/Username must not contain leading or trailing spaces/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: '€' }, + }); + + await findAllByText( + /Username must contain only letters, numbers, spaces, punctuation, and symbols/i + ); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '111' } }); + + await findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await findByLabelText('Password'), { target: { value: '123456' } }); + fireEvent.change(await findByLabelText('Confirm password'), { target: { value: '111' } }); + + await findAllByText(/Passwords do not match/i); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx new file mode 100644 index 0000000000000..6842ddb774bda --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/create_user_page.tsx @@ -0,0 +1,44 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { + EuiHorizontalRule, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { UserForm } from './user_form'; + +export const CreateUserPage: FunctionComponent = () => { + const history = useHistory(); + const backToUsers = () => history.push('/'); + + return ( + + + + +

+ +

+
+
+
+ + + + +
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss deleted file mode 100644 index 727fac4782752..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.scss +++ /dev/null @@ -1,6 +0,0 @@ -.secUsersEditPage__content { - max-width: 460px; - margin-left: auto; - margin-right: auto; - flex-grow: 0; -} diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 5e8c9f2d14a4c..f065c45d7080c 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -4,228 +4,440 @@ * you may not use this file except in compliance with the Elastic License. */ -import { act } from '@testing-library/react'; -import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { User, Role } from '../../../../common/model'; -import { ReactWrapper } from 'enzyme'; -import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; +import { + fireEvent, + render, + waitFor, + waitForElementToBeRemoved, + within, +} from '@testing-library/react'; +import { createMemoryHistory } from 'history'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; import { securityMock } from '../../../mocks'; -import { rolesAPIClientMock } from '../../roles/index.mock'; -import { userAPIClientMock } from '../index.mock'; -import { findTestSubject } from '@kbn/test/jest'; - -const createUser = (username: string, roles = ['idk', 'something']) => { - const user: User = { - username, - full_name: 'my full name', - email: 'foo@bar.com', - roles, - enabled: true, - }; - - if (username === 'reserved_user') { - user.metadata = { - _reserved: true, - }; - } - - if (username === 'deprecated_user') { - user.metadata = { - _reserved: true, - _deprecated: true, - _deprecated_reason: 'beacuse I said so.', - }; - } - - return user; +import { Providers } from '../users_management_app'; +import { EditUserPage } from './edit_user_page'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const userMock = { + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], }; -const buildClients = (user: User) => { - const apiClient = userAPIClientMock.create(); - apiClient.getUser.mockResolvedValue(user); +describe('EditUserPage', () => { + it('warns when viewing deactivated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByText } = render( + + + + ); - const rolesAPIClient = rolesAPIClientMock.create(); - rolesAPIClient.getRoles.mockImplementation(() => { - return Promise.resolve([ - { - name: 'role 1', - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: [], - }, - kibana: [], - }, - { - name: 'role 2', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + await findByText(/User has been deactivated/i); + }); + + it('warns when viewing deprecated user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { + _reserved: true, + _deprecated: true, + _deprecated_reason: 'Use [new_user] instead.', }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is deprecated/i); + await findByText(/Use .new_user. instead/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when viewing built-in user', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + metadata: { _reserved: true, _deprecated: false }, + }); + coreStart.http.get.mockResolvedValueOnce([]); + + const { findByRole, findByText } = render( + + + + ); + + await findByText(/User is built in/i); + + fireEvent.click(await findByRole('button', { name: 'Back to users' })); + + await waitFor(() => expect(history.location.pathname).toBe('/')); + }); + + it('warns when selecting deprecated role', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ + ...userMock, + enabled: false, + roles: ['deprecated_role'], + }); + coreStart.http.get.mockResolvedValueOnce([ { - name: 'deprecated-role', - elasticsearch: { - cluster: [], - indices: [], - run_as: ['bar'], - }, - kibana: [], + name: 'deprecated_role', metadata: { + _reserved: true, _deprecated: true, + _deprecated_reason: 'Use [new_role] instead.', }, }, - ] as Role[]); + ]); + + const { findByText } = render( + + + + ); + + await findByText(/Role .deprecated_role. is deprecated. Use .new_role. instead/i); }); - return { apiClient, rolesAPIClient }; -}; + it('updates user when submitting form and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; -function buildSecuritySetup() { - const securitySetupMock = securityMock.createSetup(); - securitySetupMock.authc.getCurrentUser.mockResolvedValue( - mockAuthenticatedUser(createUser('current_user')) - ); - return securitySetupMock; -} + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); -function expectSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(1); -} + const { findByRole, findByLabelText } = render( + + + + ); -function expectMissingSaveButton(wrapper: ReactWrapper) { - expect(wrapper.find('EuiButton[data-test-subj="userFormSaveButton"]')).toHaveLength(0); -} + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(history.location.pathname).toBe('/'); + }); + }); -describe('EditUserPage', () => { - const history = scopedHistoryMock.create(); - - it('allows reserved users to be viewed', async () => { - const user = createUser('reserved_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('warns when user form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); + + const { findByRole, findByLabelText } = render( + + + ); - await waitForRender(wrapper); + fireEvent.change(await findByLabelText('Full name'), { target: { value: 'John Doe' } }); + fireEvent.change(await findByLabelText('Email address'), { + target: { value: 'jdoe@elastic.co' }, + }); + fireEvent.click(await findByRole('button', { name: 'Update user' })); + + await waitFor(() => { + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security/users/jdoe'); + expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe', { + body: JSON.stringify({ + ...userMock, + full_name: 'John Doe', + email: 'jdoe@elastic.co', + }), + }); + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: "Could not update user 'jdoe'", + }); + expect(history.location.pathname).toBe('/edit/jdoe'); + }); + }); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + it('changes password of other user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockResolvedValueOnce({}); - expectMissingSaveButton(wrapper); + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = getByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(within(dialog).getByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(within(dialog).getByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + }), + }); }); - it('allows new users to be created', async () => { - const user = createUser(''); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('changes password of current user when submitting form and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/password', { + body: JSON.stringify({ + newPassword: 'changeme', + password: '123456', + }), + }); + }); + + it('warns when change password form submission fails', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce( + mockAuthenticatedUser({ ...userMock, username: 'elastic' }) + ); + coreStart.http.post.mockRejectedValueOnce(new Error('Error message')); - expect(apiClient.getUser).toBeCalledTimes(0); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(0); + const { findByRole } = render( + + + + ); - expectSaveButton(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: 'changeme' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); + + await waitFor(() => { + expect(coreStart.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'Error message', + title: 'Could not change password', + }); + }); }); - it('allows existing users to be edited', async () => { - const user = createUser('existing_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - const wrapper = mountWithIntl( - + it('validates change password form', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + authc.getCurrentUser.mockResolvedValueOnce(mockAuthenticatedUser(userMock)); + coreStart.http.post.mockResolvedValueOnce({}); + + const { findByRole } = render( + + + ); - await waitForRender(wrapper); + fireEvent.click(await findByRole('button', { name: 'Change password' })); + + const dialog = await findByRole('dialog'); + fireEvent.click(await within(dialog).findByRole('button', { name: 'Change password' })); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + await within(dialog).findByText(/Enter your current password/i); + await within(dialog).findByText(/Enter a new password/i); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(0); - expectSaveButton(wrapper); + fireEvent.change(await within(dialog).findByLabelText('Current password'), { + target: { value: 'changeme' }, + }); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Password must be at least 6 characters/i); + + fireEvent.change(await within(dialog).findByLabelText('New password'), { + target: { value: '123456' }, + }); + fireEvent.change(await within(dialog).findByLabelText('Confirm password'), { + target: { value: '111' }, + }); + + await within(dialog).findAllByText(/Passwords do not match/i); }); - it('warns when viewing a depreciated user', async () => { - const user = createUser('deprecated_user'); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('deactivates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + ); - await waitForRender(wrapper); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + fireEvent.click(await findByRole('button', { name: 'Deactivate user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Deactivate user' })); - expect(findTestSubject(wrapper, 'deprecatedUserWarning')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_disable'); }); - it('warns when user is assigned a deprecated role', async () => { - const user = createUser('existing_user', ['deprecated-role']); - const { apiClient, rolesAPIClient } = buildClients(user); - const securitySetup = buildSecuritySetup(); - - const wrapper = mountWithIntl( - + it('activates user when confirming and closes dialog', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce({ ...userMock, enabled: false }); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.post.mockResolvedValueOnce({}); + + const { getByRole, findAllByRole } = render( + + + ); - await waitForRender(wrapper); + const [enableButton] = await findAllByRole('button', { name: 'Activate user' }); + fireEvent.click(enableButton); - expect(apiClient.getUser).toBeCalledTimes(1); - expect(securitySetup.authc.getCurrentUser).toBeCalledTimes(1); + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Activate user' })); - expect(findTestSubject(wrapper, 'hasDeprecatedRolesAssignedHelpText')).toHaveLength(1); + await waitForElementToBeRemoved(() => getByRole('dialog')); + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/users/jdoe/_enable'); }); -}); -async function waitForRender(wrapper: ReactWrapper) { - await act(async () => { - await nextTick(); - wrapper.update(); + it('deletes user when confirming and redirects back', async () => { + const coreStart = coreMock.createStart(); + const history = createMemoryHistory({ initialEntries: ['/edit/jdoe'] }); + const authc = securityMock.createSetup().authc; + + coreStart.http.get.mockResolvedValueOnce(userMock); + coreStart.http.get.mockResolvedValueOnce([]); + coreStart.http.delete.mockResolvedValueOnce({}); + + const { getByRole, findByRole } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Delete user' })); + + const dialog = getByRole('dialog'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Delete user' })); + + expect(coreStart.http.delete).toHaveBeenLastCalledWith('/internal/security/users/jdoe'); + await waitFor(() => { + expect(history.location.pathname).toBe('/'); + }); }); -} +}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index dc0c3336cb85f..68c01bf509b0d 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -4,615 +4,315 @@ * you may not use this file except in compliance with the Elastic License. */ -import './edit_user_page.scss'; - -import { get } from 'lodash'; -import React, { Component, Fragment, ChangeEvent } from 'react'; +import React, { FunctionComponent, useState, useEffect } from 'react'; import { + EuiAvatar, EuiButton, EuiCallOut, - EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, - EuiLink, - EuiTitle, - EuiForm, - EuiFormRow, - EuiIcon, - EuiText, - EuiFieldText, + EuiHorizontalRule, EuiPageContent, + EuiPageContentBody, EuiPageContentHeader, EuiPageContentHeaderSection, - EuiPageContentBody, - EuiHorizontalRule, + EuiPanel, EuiSpacer, + EuiText, + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { PublicMethodsOf } from '@kbn/utility-types'; -import { NotificationsStart, ScopedHistory } from 'src/core/public'; -import { User, EditUser, Role, isRoleDeprecated } from '../../../../common/model'; -import { AuthenticationServiceSetup } from '../../../authentication'; -import { RolesAPIClient } from '../../roles'; -import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; -import { UserValidator, UserValidationResult } from './validate_user'; -import { RoleComboBox } from '../../role_combo_box'; -import { isUserDeprecated, getExtendedUserDeprecationNotice, isUserReserved } from '../user_utils'; +import { useHistory } from 'react-router-dom'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { getUserDisplayName } from '../../../../common/model'; +import { isUserDeprecated, isUserReserved } from '../user_utils'; +import { UserForm } from './user_form'; +import { ChangePasswordFlyout } from './change_password_flyout'; +import { ConfirmDisableUsers } from './confirm_disable_users'; +import { ConfirmEnableUsers } from './confirm_enable_users'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import { UserAPIClient } from '..'; -interface Props { - username?: string; - userAPIClient: PublicMethodsOf; - rolesAPIClient: PublicMethodsOf; - authc: AuthenticationServiceSetup; - notifications: NotificationsStart; - history: ScopedHistory; -} - -interface State { - isLoaded: boolean; - isNewUser: boolean; - currentUser: User | null; - showChangePasswordForm: boolean; - showDeleteConfirmation: boolean; - user: EditUser; - roles: Role[]; - selectedRoles: readonly string[]; - formError: UserValidationResult | null; +export interface EditUserPageProps { + username: string; } -export class EditUserPage extends Component { - private validator: UserValidator; - - constructor(props: Props) { - super(props); - this.validator = new UserValidator({ shouldValidate: false }); - this.state = { - isLoaded: false, - isNewUser: true, - currentUser: null, - showChangePasswordForm: false, - showDeleteConfirmation: false, - user: { - email: '', - username: '', - full_name: '', - roles: [], - enabled: true, - password: '', - confirmPassword: '', - }, - roles: [], - selectedRoles: [], - formError: null, - }; - } - - public async componentDidMount() { - await this.setCurrentUser(); - } - - public async componentDidUpdate(prevProps: Props) { - if (prevProps.username !== this.props.username) { - await this.setCurrentUser(); +export type EditUserPageAction = + | 'changePassword' + | 'disableUser' + | 'enableUser' + | 'deleteUser' + | 'none'; + +export const EditUserPage: FunctionComponent = ({ username }) => { + const { services } = useKibana(); + const history = useHistory(); + const [{ value: user, error }, getUser] = useAsyncFn( + () => new UserAPIClient(services.http!).getUser(username), + [services.http] + ); + const [action, setAction] = useState('none'); + + const backToUsers = () => history.push('/'); + + useEffect(() => { + getUser(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + if (error) { + backToUsers(); } - } + }, [error]); // eslint-disable-line react-hooks/exhaustive-deps - private backToUserList() { - this.props.history.push('/'); + if (!user) { + return null; } - private async setCurrentUser() { - const { username, userAPIClient, rolesAPIClient, notifications, authc } = this.props; - let { user, currentUser } = this.state; - if (username) { - try { - user = { - ...(await userAPIClient.getUser(username)), - password: '', - confirmPassword: '', - }; - currentUser = await authc.getCurrentUser(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { - defaultMessage: 'Error loading user', - }), - text: get(err, 'body.message') || err.message, - }); - return this.backToUserList(); - } - } - - let roles: Role[] = []; - try { - roles = await rolesAPIClient.getRoles(); - } catch (err) { - notifications.toasts.addDanger({ - title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { - defaultMessage: 'Error loading roles', - }), - text: get(err, 'body.message') || err.message, - }); - } - - this.setState({ - isLoaded: true, - isNewUser: !username, - currentUser, - user, - roles, - selectedRoles: user.roles || [], - }); - } - - private handleDelete = (usernames: string[], errors: string[]) => { - if (errors.length === 0) { - this.backToUserList(); - } - }; - - private saveUser = async () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.user, this.state.isNewUser); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - const { userAPIClient } = this.props; - const { user, isNewUser, selectedRoles } = this.state; - const userToSave: EditUser = { ...user }; - if (!isNewUser) { - delete userToSave.password; - } - delete userToSave.confirmPassword; - userToSave.roles = [...selectedRoles]; - try { - await userAPIClient.saveUser(userToSave); - this.props.notifications.toasts.addSuccess( - i18n.translate( - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', - { - defaultMessage: 'Saved user {message}', - values: { message: user.username }, - } - ) - ); - - this.backToUserList(); - } catch (e) { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { - defaultMessage: 'Error saving user: {message}', - values: { message: get(e, 'body.message', 'Unknown error') }, - }) - ); - } - } - }; - - private passwordFields = () => { - return ( - - - - - - - - - ); - }; - - private changePasswordForm = () => { - const { showChangePasswordForm, user, currentUser } = this.state; - - const userIsLoggedInUser = Boolean( - currentUser && user.username && user.username === currentUser.username - ); - - if (!showChangePasswordForm) { - return null; - } - return ( - + const isReservedUser = isUserReserved(user); + const isDeprecatedUser = isUserDeprecated(user); + const displayName = getUserDisplayName(user); + + return ( + + + + + + + + + +

{displayName}

+
+ {user.email} +
+
+
+
+ - {user.username === 'kibana' || user.username === 'kibana_system' ? ( - + {isDeprecatedUser ? ( + <> + } + iconType="alert" color="warning" - iconType="help" > -

+ {user.metadata?._deprecated_reason?.replace(/\[(.+)\]/, "'$1'")} + + + + ) : isReservedUser ? ( + <> + + } + iconType="lock" + /> + + + ) : user.enabled === false ? ( + <> + -

+ } + > + setAction('enableUser')} size="s"> + +
-
- ) : null} - + ) : undefined} + + -
- ); - }; - - private toggleChangePasswordForm = () => { - const { showChangePasswordForm } = this.state; - this.setState({ showChangePasswordForm: !showChangePasswordForm }); - }; - - private onUsernameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - username: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - private onEmailChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - email: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onFullNameChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - full_name: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - password: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onConfirmPasswordChange = (e: ChangeEvent) => { - const user = { - ...this.state.user, - confirmPassword: e.target.value || '', - }; - const formError = this.validator.validateForSave(user, this.state.isNewUser); - - this.setState({ - user, - formError, - }); - }; - - private onRolesChange = (selectedRoles: string[]) => { - this.setState({ - selectedRoles, - }); - }; - - private cannotSaveUser = () => { - const { user, isNewUser } = this.state; - const result = this.validator.validateForSave(user, isNewUser); - return result.isInvalid; - }; - - private onCancelDelete = () => { - this.setState({ showDeleteConfirmation: false }); - }; - - public render() { - const { - user, - selectedRoles, - roles, - showChangePasswordForm, - isNewUser, - showDeleteConfirmation, - } = this.state; - const reserved = isUserReserved(user); - if (!user || !roles) { - return null; - } - - if (!this.state.isLoaded) { - return null; - } - - const hasAnyDeprecatedRolesAssigned = selectedRoles.some((selected) => { - const role = roles.find((r) => r.name === selected); - return role && isRoleDeprecated(role); - }); + {action === 'changePassword' ? ( + setAction('none')} + onSuccess={() => setAction('none')} + /> + ) : action === 'disableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'enableUser' ? ( + setAction('none')} + onSuccess={() => { + setAction('none'); + getUser(); + }} + /> + ) : action === 'deleteUser' ? ( + setAction('none')} + onSuccess={backToUsers} + /> + ) : undefined} - const roleHelpText = hasAnyDeprecatedRolesAssigned ? ( - - - - ) : undefined; + + - return ( -
- - - - -

- {isNewUser ? ( + + + + + + + + + + + + + + setAction('changePassword')} size="s"> + + + + + + + + {user.enabled === false ? ( + + + + + - ) : ( + + - )} -

-
-
- {reserved && ( - - - - )} -
- - {reserved && ( - - -

+ + + + + setAction('enableUser')} size="s"> + + + + + + ) : ( + + + + + -

-
- -
- )} - - {isUserDeprecated(user) && ( - - - - - )} - - {showDeleteConfirmation ? ( - - ) : null} - - - - - - {isNewUser ? this.passwordFields() : null} - {reserved ? null : ( - - - - - - - - - )} - - - - - {isNewUser || showChangePasswordForm ? null : ( - - + + - - - )} - {this.changePasswordForm()} - - - - {reserved && ( - this.backToUserList()}> + + + + + setAction('disableUser')} size="s"> - )} - {reserved ? null : ( - - - this.saveUser()} - > - {isNewUser ? ( - - ) : ( - - )} - - - - this.backToUserList()} - > + + + + )} + + {!isReservedUser && ( + <> + + + + + + - - - - {isNewUser || reserved ? null : ( - - { - this.setState({ showDeleteConfirmation: true }); - }} - data-test-subj="userFormDeleteButton" - color="danger" - > - - - - )} - - )} - -
-
-
- ); - } -} + + + + + + + + setAction('deleteUser')} size="s" color="danger"> + + + + + + + )} + + + ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts index 92eb17b9ebd36..30069d8e97c31 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/index.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/index.ts @@ -5,3 +5,4 @@ */ export { EditUserPage } from './edit_user_page'; +export { CreateUserPage } from './create_user_page'; diff --git a/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx new file mode 100644 index 0000000000000..daa488d674fbb --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/user_form.tsx @@ -0,0 +1,466 @@ +/* + * 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, { FunctionComponent, useEffect, useCallback } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiDescribedFormGroup, + EuiFieldPassword, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTextColor, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { throttle } from 'lodash'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { User, Role, isRoleDeprecated } from '../../../../common/model'; +import { NAME_REGEX, MAX_NAME_LENGTH } from '../../../../common/constants'; +import { useForm, ValidationErrors } from '../../../components/use_form'; +import { DocLink } from '../../../components/doc_link'; +import { RolesAPIClient } from '../../roles'; +import { RoleComboBox } from '../../role_combo_box'; +import { UserAPIClient } from '..'; + +export const THROTTLE_USERS_WAIT = 10000; + +export interface UserFormValues { + username?: string; + full_name: string; + email: string; + password?: string; + confirm_password?: string; + roles: readonly string[]; +} + +export interface UserFormProps { + isNewUser?: boolean; + isReservedUser?: boolean; + isCurrentUser?: boolean; + defaultValues?: UserFormValues; + onCancel(): void; + onSuccess?(): void; +} + +const defaultDefaultValues: UserFormValues = { + username: '', + password: '', + confirm_password: '', + full_name: '', + email: '', + roles: [], +}; + +export const UserForm: FunctionComponent = ({ + isNewUser = false, + isReservedUser = false, + defaultValues = defaultDefaultValues, + onSuccess, + onCancel, +}) => { + const { services } = useKibana(); + + const [rolesState, getRoles] = useAsyncFn(() => new RolesAPIClient(services.http!).getRoles(), [ + services.http, + ]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const getUsersThrottled = useCallback( + throttle(() => new UserAPIClient(services.http!).getUsers(), THROTTLE_USERS_WAIT), + [services.http] + ); + + const [form, eventHandlers] = useForm({ + onSubmit: async (values) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { password, confirm_password, ...rest } = values; + const user = isNewUser ? { password, ...rest } : rest; + try { + await new UserAPIClient(services.http!).saveUser(user as User); + services.notifications!.toasts.addSuccess( + isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createSuccessMessage', { + defaultMessage: "Created user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateSuccessMessage', { + defaultMessage: "Updated user '{username}'", + values: { username: user.username }, + }) + ); + onSuccess?.(); + } catch (error) { + services.notifications!.toasts.addDanger({ + title: isNewUser + ? i18n.translate('xpack.security.management.users.userForm.createErrorMessage', { + defaultMessage: "Could not create user '{username}'", + values: { username: user.username }, + }) + : i18n.translate('xpack.security.management.users.userForm.updateErrorMessage', { + defaultMessage: "Could not update user '{username}'", + values: { username: user.username }, + }), + text: (error as any).body?.message || error.message, + }); + throw error; + } + }, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (isNewUser) { + if (!values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameRequiredError', + { + defaultMessage: 'Enter a username.', + } + ); + } else if (values.username.length > MAX_NAME_LENGTH) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameMaxLengthError', + { + defaultMessage: 'Username must not exceed {maxLength} characters.', + values: { maxLength: MAX_NAME_LENGTH }, + } + ); + } else if (values.username.trim() !== values.username) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameWhitespaceError', + { + defaultMessage: `Username must not contain leading or trailing spaces.`, + } + ); + } else if (!values.username.match(NAME_REGEX)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameInvalidError', + { + defaultMessage: + 'Username must contain only letters, numbers, spaces, punctuation, and symbols.', + } + ); + } else { + try { + const users = await getUsersThrottled(); + if (users.some((user) => user.username === values.username)) { + errors.username = i18n.translate( + 'xpack.security.management.users.userForm.usernameTakenError', + { + defaultMessage: "User '{username}' already exists.", + values: { username: values.username }, + } + ); + } + } catch (error) {} // eslint-disable-line no-empty + } + + if (!values.password) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordRequiredError', + { + defaultMessage: 'Enter a password.', + } + ); + } else if (values.password.length < 6) { + errors.password = i18n.translate( + 'xpack.security.management.users.userForm.passwordInvalidError', + { + defaultMessage: 'Password must be at least 6 characters.', + } + ); + } else if (!values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordRequiredError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } else if (values.password !== values.confirm_password) { + errors.confirm_password = i18n.translate( + 'xpack.security.management.users.userForm.confirmPasswordInvalidError', + { + defaultMessage: 'Passwords do not match.', + } + ); + } + } + + return errors; + }, + defaultValues, + }); + + useEffect(() => { + form.reset(defaultValues); + }, [defaultValues]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + getRoles(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const availableRoles = rolesState.value ?? []; + const selectedRoleNames = form.values.roles ?? []; + const deprecatedRoles = selectedRoleNames.reduce((roles, name) => { + const role = availableRoles.find((r) => r.name === name); + if (role && isRoleDeprecated(role)) { + roles.push(role); + } + return roles; + }, []); + + return ( + + + + + } + description={i18n.translate('xpack.security.management.users.userForm.profileDescription', { + defaultMessage: 'Provide personal details.', + })} + > + + + + + {!isReservedUser ? ( + <> + + + + + + + + ) : undefined} + + + {isNewUser ? ( + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.passwordDescription', + { + defaultMessage: 'Protect your data with a strong password.', + } + )} + > + + + + + + + + ) : null} + + + + + } + description={i18n.translate( + 'xpack.security.management.users.userForm.privilegesDescription', + { + defaultMessage: 'Assign roles to manage access and permissions.', + } + )} + > + 0 ? ( + + {deprecatedRoles.map((role) => ( +

+ +

+ ))} +
+ ) : ( + + + + ) + } + > + form.setValue('roles', value)} + isLoading={rolesState.loading} + isDisabled={isReservedUser} + /> +
+ + + {isReservedUser ? ( + + + + + + + + ) : ( + + + + {isNewUser ? ( + + ) : ( + + )} + + + + + + + + + )} +
+
+ ); +}; diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts deleted file mode 100644 index 6050e1868a759..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts +++ /dev/null @@ -1,128 +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 { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../../../common/model'; - -function expectValid(result: UserValidationResult) { - expect(result.isInvalid).toBe(false); -} - -function expectInvalid(result: UserValidationResult) { - expect(result.isInvalid).toBe(true); -} - -describe('UserValidator', () => { - describe('#validateUsername', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateUsername({} as User)); - }); - - it(`returns 'invalid' if username is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validateUsername({} as User)); - }); - - it(`returns 'invalid' if username contains invalid characters`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: '!@#$%^&*()', - } as User) - ); - }); - - it(`returns 'valid' for correct usernames`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateUsername({ - username: 'my_user', - } as User) - ); - }); - }); - - describe('#validateEmail', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateEmail({} as EditUser)); - }); - - it(`returns 'valid' if email is missing`, () => { - expectValid(new UserValidator({ shouldValidate: true }).validateEmail({} as EditUser)); - }); - - it(`returns 'invalid' for invalid emails`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'asf', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct emails`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateEmail({ - email: 'foo@bar.co', - } as EditUser) - ); - }); - }); - - describe('#validatePassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' if password is missing`, () => { - expectInvalid(new UserValidator({ shouldValidate: true }).validatePassword({} as EditUser)); - }); - - it(`returns 'invalid' for invalid password`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'short', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validatePassword({ - password: 'changeme', - } as EditUser) - ); - }); - }); - - describe('#validateConfirmPassword', () => { - it(`returns 'valid' if validation is disabled`, () => { - expectValid(new UserValidator().validateConfirmPassword({} as EditUser)); - }); - - it(`returns 'invalid' if confirm password is missing`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - } as EditUser) - ); - }); - - it(`returns 'invalid' for mismatched passwords`, () => { - expectInvalid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeyou', - } as EditUser) - ); - }); - - it(`returns 'valid' for correct passwords`, () => { - expectValid( - new UserValidator({ shouldValidate: true }).validateConfirmPassword({ - password: 'changeme', - confirmPassword: 'changeme', - } as EditUser) - ); - }); - }); -}); diff --git a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts deleted file mode 100644 index 5edd96c68bf0d..0000000000000 --- a/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts +++ /dev/null @@ -1,142 +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 { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../../../common/model'; - -interface UserValidatorOptions { - shouldValidate?: boolean; -} - -export interface UserValidationResult { - isInvalid: boolean; - error?: string; -} - -const validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -const validUsernameRegex = /[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*/; - -export class UserValidator { - private shouldValidate?: boolean; - - constructor(options: UserValidatorOptions = {}) { - this.shouldValidate = options.shouldValidate; - } - - public enableValidation() { - this.shouldValidate = true; - } - - public disableValidation() { - this.shouldValidate = false; - } - - public validateUsername(user: User): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { username } = user; - if (!username) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.requiredUsernameErrorMessage', { - defaultMessage: 'Username is required', - }) - ); - } else if (username && !username.match(validUsernameRegex)) { - return invalid( - i18n.translate( - 'xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage', - { - defaultMessage: - 'Username must begin with a letter or underscore and contain only letters, underscores, and numbers', - } - ) - ); - } - - return valid(); - } - - public validateEmail(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { email } = user; - if (email && !email.match(validEmailRegex)) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.validEmailRequiredErrorMessage', { - defaultMessage: 'Email address is invalid', - }) - ); - } - return valid(); - } - - public validatePassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password } = user; - if (!password || password.length < 6) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordLengthErrorMessage', { - defaultMessage: 'Password must be at least 6 characters', - }) - ); - } - return valid(); - } - - public validateConfirmPassword(user: EditUser): UserValidationResult { - if (!this.shouldValidate) { - return valid(); - } - - const { password, confirmPassword } = user; - if (password && confirmPassword !== null && password !== confirmPassword) { - return invalid( - i18n.translate('xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage', { - defaultMessage: 'Passwords do not match', - }) - ); - } - return valid(); - } - - public validateForSave(user: EditUser, isNewUser: boolean): UserValidationResult { - const { isInvalid: isUsernameInvalid } = this.validateUsername(user); - const { isInvalid: isEmailInvalid } = this.validateEmail(user); - let isPasswordInvalid = false; - let isConfirmPasswordInvalid = false; - - if (isNewUser) { - isPasswordInvalid = this.validatePassword(user).isInvalid; - isConfirmPasswordInvalid = this.validateConfirmPassword(user).isInvalid; - } - - if (isUsernameInvalid || isEmailInvalid || isPasswordInvalid || isConfirmPasswordInvalid) { - return invalid(); - } - - return valid(); - } -} - -function invalid(error?: string): UserValidationResult { - return { - isInvalid: true, - error, - }; -} - -function valid(): UserValidationResult { - return { - isInvalid: false, - }; -} diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts index 7223f78d57fdc..54c7ae8f4ae3b 100644 --- a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts @@ -9,6 +9,8 @@ export const userAPIClientMock = { getUsers: jest.fn(), getUser: jest.fn(), deleteUser: jest.fn(), + enableUser: jest.fn(), + disableUser: jest.fn(), saveUser: jest.fn(), changePassword: jest.fn(), }), diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts index 61dd09d2c5e3d..b96596ba7c653 100644 --- a/x-pack/plugins/security/public/management/users/user_api_client.ts +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -30,7 +30,7 @@ export class UserAPIClient { }); } - public async changePassword(username: string, password: string, currentPassword: string) { + public async changePassword(username: string, password: string, currentPassword?: string) { const data: Record = { newPassword: password, }; @@ -42,4 +42,12 @@ export class UserAPIClient { body: JSON.stringify(data), }); } + + public async disableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_disable`); + } + + public async enableUser(username: string) { + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/_enable`); + } } diff --git a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index 37747f9a1ccfa..3b1705d2bc46b 100644 --- a/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -237,7 +237,7 @@ export class UsersGridPage extends Component { ({ - UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, -})); - -jest.mock('./edit_user', () => ({ - EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, -})); - -import { usersManagementApp } from './users_management_app'; - import { coreMock, scopedHistoryMock } from '../../../../../../src/core/public/mocks'; import { securityMock } from '../../mocks'; +import { usersManagementApp } from './users_management_app'; -async function mountApp(basePath: string, pathname: string) { - const container = document.createElement('div'); - const setBreadcrumbs = jest.fn(); +const element = document.body.appendChild(document.createElement('div')); - const unmount = await usersManagementApp - .create({ - authc: securityMock.createSetup().authc, - getStartServices: coreMock.createSetup().getStartServices as any, - }) - .mount({ - basePath, - element: container, +describe('usersManagementApp', () => { + it('renders application and sets breadcrumbs', async () => { + const { getStartServices } = coreMock.createSetup(); + const coreStartMock = coreMock.createStart(); + getStartServices.mockResolvedValue([coreStartMock, {}, {}]); + const { authc } = securityMock.createSetup(); + + const setBreadcrumbs = jest.fn(); + const history = scopedHistoryMock.create({ pathname: '/create' }); + + const unmount = await usersManagementApp.create({ authc, getStartServices }).mount({ + basePath: '/', + element, setBreadcrumbs, - history: scopedHistoryMock.create({ pathname }), + history, }); - return { unmount, container, setBreadcrumbs }; -} - -describe('usersManagementApp', () => { - it('create() returns proper management app descriptor', () => { - expect( - usersManagementApp.create({ - authc: securityMock.createSetup().authc, - getStartServices: coreMock.createSetup().getStartServices as any, - }) - ).toMatchInlineSnapshot(` - Object { - "id": "users", - "mount": [Function], - "order": 10, - "title": "Users", - } - `); - }); - - it('mount() works for the `grid` page', async () => { - const { setBreadcrumbs, container, unmount } = await mountApp('/', '/'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }]); - expect(container).toMatchInlineSnapshot(` -
- Users Page: {"notifications":{"toasts":{}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `create user` page', async () => { - const { setBreadcrumbs, container, unmount } = await mountApp('/', '/edit'); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Users' }, { text: 'Create' }]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} -
- `); - - unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - it('mount() works for the `edit user` page', async () => { - const userName = 'foo@bar.com'; - - const { setBreadcrumbs, container, unmount } = await mountApp('/', `/edit/${userName}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { href: `/edit/${encodeURIComponent(userName)}`, text: userName }, + expect(setBreadcrumbs).toHaveBeenLastCalledWith([ + { href: '/', text: 'Users' }, + { href: '/create', text: 'Create' }, ]); - expect(container).toMatchInlineSnapshot(` -
- User Edit Page: {"authc":{},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"username":"foo@bar.com","history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/foo@bar.com","search":"","hash":""}}} -
- `); unmount(); - - expect(container).toMatchInlineSnapshot(`
`); - }); - - const usernames = ['foo@bar.com', 'foo&bar.com', 'some 安全性 user']; - usernames.forEach((username) => { - it( - 'mount() properly encodes user name in `edit user` page link in breadcrumbs for user ' + - username, - async () => { - const { setBreadcrumbs } = await mountApp('/', `/edit/${username}`); - - expect(setBreadcrumbs).toHaveBeenCalledTimes(1); - expect(setBreadcrumbs).toHaveBeenCalledWith([ - { href: `/`, text: 'Users' }, - { - href: `/edit/${encodeURIComponent(username)}`, - text: username, - }, - ]); - } - ); }); }); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx index 2f16f85d5fcae..cbb303d1a128d 100644 --- a/x-pack/plugins/security/public/management/users/users_management_app.tsx +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -4,14 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Route, Switch, useParams } from 'react-router-dom'; +import { Router, Route, Switch, Redirect, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; -import { StartServicesAccessor } from 'src/core/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import { StartServicesAccessor, CoreStart } from '../../../../../../src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { AuthenticationServiceSetup } from '../../authentication'; import { PluginStartDependencies } from '../../plugin'; +import { + BreadcrumbsProvider, + BreadcrumbsChangeHandler, + Breadcrumb, + getDocTitle, +} from '../../components/breadcrumb'; +import { AuthenticationProvider } from '../../components/use_current_user'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -19,6 +29,10 @@ interface CreateParams { getStartServices: StartServicesAccessor; } +interface EditUserParams { + username: string; +} + export const usersManagementApp = Object.freeze({ id: 'users', create({ authc, getStartServices }: CreateParams) { @@ -27,18 +41,10 @@ export const usersManagementApp = Object.freeze({ order: 10, title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), async mount({ element, setBreadcrumbs, history }) { - const [coreStart] = await getStartServices(); - const usersBreadcrumbs = [ - { - text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), - href: `/`, - }, - ]; - const [ - [{ http, notifications, i18n: i18nStart }], + [coreStart], { UsersGridPage }, - { EditUserPage }, + { CreateUserPage, EditUserPage }, { UserAPIClient }, { RolesAPIClient }, ] = await Promise.all([ @@ -49,64 +55,61 @@ export const usersManagementApp = Object.freeze({ import('../roles'), ]); - const userAPIClient = new UserAPIClient(http); - const rolesAPIClient = new RolesAPIClient(http); - const UsersGridPageWithBreadcrumbs = () => { - setBreadcrumbs(usersBreadcrumbs); - return ( - - ); - }; - - const EditUserPageWithBreadcrumbs = () => { - const { username } = useParams<{ username?: string }>(); - - // Additional decoding is a workaround for a bug in react-router's version of the `history` module. - // See https://github.com/elastic/kibana/issues/82440 - const decodedUsername = username ? tryDecodeURIComponent(username) : undefined; - - setBreadcrumbs([ - ...usersBreadcrumbs, - username - ? { text: decodedUsername, href: `/edit/${encodeURIComponent(username)}` } - : { - text: i18n.translate('xpack.security.users.createBreadcrumb', { - defaultMessage: 'Create', - }), - }, - ]); - - return ( - - ); - }; - render( - - + { + setBreadcrumbs(breadcrumbs); + coreStart.chrome.docTitle.change(getDocTitle(breadcrumbs)); + }} + > + - + - - + + + + + + ) => { + // Additional decoding is a workaround for a bug in react-router's version of the `history` module. + // See https://github.com/elastic/kibana/issues/82440 + const username = tryDecodeURIComponent(props.match.params.username); + return ( + + + + ); + }} + /> + + - - , + + , element ); @@ -117,3 +120,28 @@ export const usersManagementApp = Object.freeze({ } as RegisterManagementAppArgs; }, }); + +export interface ProvidersProps { + services: CoreStart; + history: History; + authc: AuthenticationServiceSetup; + onChange?: BreadcrumbsChangeHandler; +} + +export const Providers: FunctionComponent = ({ + services, + history, + authc, + onChange, + children, +}) => ( + + + + + {children} + + + + +); diff --git a/x-pack/plugins/security/server/routes/users/create_or_update.ts b/x-pack/plugins/security/server/routes/users/create_or_update.ts index a98848a583500..bdb6e89719037 100644 --- a/x-pack/plugins/security/server/routes/users/create_or_update.ts +++ b/x-pack/plugins/security/server/routes/users/create_or_update.ts @@ -30,12 +30,7 @@ export function defineCreateOrUpdateUserRoutes({ router }: RouteDefinitionParams try { await context.core.elasticsearch.client.asCurrentUser.security.putUser({ username: request.params.username, - // Omit `username`, `enabled` and all fields with `null` value. - body: Object.fromEntries( - Object.entries(request.body).filter( - ([key, value]) => value !== null && key !== 'enabled' && key !== 'username' - ) - ), + body: request.body, }); return response.ok({ body: request.body }); diff --git a/x-pack/plugins/security/server/routes/users/disable.ts b/x-pack/plugins/security/server/routes/users/disable.ts new file mode 100644 index 0000000000000..45e1f63149e1a --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/disable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineDisableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_disable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.disableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/enable.ts b/x-pack/plugins/security/server/routes/users/enable.ts new file mode 100644 index 0000000000000..0f4e15c953a42 --- /dev/null +++ b/x-pack/plugins/security/server/routes/users/enable.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { RouteDefinitionParams } from '../index'; +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; + +export function defineEnableUserRoutes({ router }: RouteDefinitionParams) { + router.post( + { + path: '/internal/security/users/{username}/_enable', + validate: { + params: schema.object({ username: schema.string({ minLength: 1, maxLength: 1024 }) }), + }, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + await context.core.elasticsearch.client.asCurrentUser.security.enableUser({ + username: request.params.username, + }); + + return response.noContent(); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/users/index.ts b/x-pack/plugins/security/server/routes/users/index.ts index 931af0734b416..473b3459ad4e1 100644 --- a/x-pack/plugins/security/server/routes/users/index.ts +++ b/x-pack/plugins/security/server/routes/users/index.ts @@ -9,6 +9,8 @@ import { defineGetUserRoutes } from './get'; import { defineGetAllUsersRoutes } from './get_all'; import { defineCreateOrUpdateUserRoutes } from './create_or_update'; import { defineDeleteUserRoutes } from './delete'; +import { defineDisableUserRoutes } from './disable'; +import { defineEnableUserRoutes } from './enable'; import { defineChangeUserPasswordRoutes } from './change_password'; export function defineUsersRoutes(params: RouteDefinitionParams) { @@ -16,5 +18,7 @@ export function defineUsersRoutes(params: RouteDefinitionParams) { defineGetAllUsersRoutes(params); defineCreateOrUpdateUserRoutes(params); defineDeleteUserRoutes(params); + defineDisableUserRoutes(params); + defineEnableUserRoutes(params); defineChangeUserPasswordRoutes(params); } diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index 324e376c32d95..14ad0561928f8 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -13,6 +13,7 @@ import { Logger } from '../../../../src/core/server'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; import moment from 'moment'; +import uuid from 'uuid'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -133,7 +134,7 @@ describe('TaskPool', () => { const result = await pool.run([mockTask(), taskFailedToRun, mockTask()]); expect(logger.debug).toHaveBeenCalledWith( - 'Task TaskType "shooooo" failed in attempt to run: Saved object [task/foo] not found' + `Task TaskType "shooooo" failed in attempt to run: Saved object [task/${taskFailedToRun.id}] not found` ); expect(logger.warn).not.toHaveBeenCalled(); @@ -203,26 +204,28 @@ describe('TaskPool', () => { sinon.assert.calledOnce(secondRun); }); - test.skip('run cancels expired tasks prior to running new tasks', async () => { + test('run cancels expired tasks prior to running new tasks', async () => { const logger = loggingSystemMock.create().get(); const pool = new TaskPool({ maxWorkers$: of(2), logger, }); - const readyToExpire = resolvable(); + const haltUntilWeAfterFirstRun = resolvable(); const taskHasExpired = resolvable(); + const haltTaskSoThatItCanBeCanceled = resolvable(); + const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); const now = new Date(); const result = await pool.run([ { - ...mockTask(), + ...mockTask({ id: '1' }), async run() { - await readyToExpire; + await haltUntilWeAfterFirstRun; this.isExpired = true; taskHasExpired.resolve(); - await sleep(10); + await haltTaskSoThatItCanBeCanceled; return asOk({ state: {} }); }, get expiration() { @@ -235,9 +238,10 @@ describe('TaskPool', () => { cancel: shouldRun, }, { - ...mockTask(), + ...mockTask({ id: '2' }), async run() { - await sleep(10); + // halt here so that we can verify that this task is counted in `occupiedWorkers` + await haltUntilWeAfterFirstRun; return asOk({ state: {} }); }, cancel: shouldNotRun, @@ -248,16 +252,19 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); - readyToExpire.resolve(); + // release first stage in task so that it has time to expire, but not complete + haltUntilWeAfterFirstRun.resolve(); await taskHasExpired; - expect(await pool.run([{ ...mockTask() }])).toBeTruthy(); + expect(await pool.run([{ ...mockTask({ id: '3' }) }])).toBeTruthy(); sinon.assert.calledOnce(shouldRun); sinon.assert.notCalled(shouldNotRun); - expect(pool.occupiedWorkers).toEqual(2); - expect(pool.availableWorkers).toEqual(0); + expect(pool.occupiedWorkers).toEqual(1); + expect(pool.availableWorkers).toEqual(1); + + haltTaskSoThatItCanBeCanceled.resolve(); expect(logger.warn).toHaveBeenCalledWith( `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).` @@ -355,10 +362,10 @@ describe('TaskPool', () => { }); } - function mockTask() { + function mockTask(overrides = {}) { return { isExpired: false, - id: 'foo', + id: uuid.v4(), cancel: async () => undefined, markTaskAsRunning: jest.fn(async () => true), run: mockRun(), @@ -377,6 +384,7 @@ describe('TaskPool', () => { createTaskRunner: jest.fn(), }; }, + ...overrides, }; } }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1bbf4b8033755..ef2149c4931fa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -16633,7 +16633,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "タグ", "xpack.savedObjectsTagging.validation.color.errorInvalid": "タグ色は有効な 16 進数値色でなければなりません", "xpack.savedObjectsTagging.validation.description.errorTooLong": "タグ説明は {length} 文字以下で入力してください", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "タグ名には、a-z、0-9、-、: のみを使用できます。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "タグ名は {length} 文字以下で入力してください", "xpack.savedObjectsTagging.validation.name.errorTooShort": "タグ名は {length} 文字以上で入力してください", "xpack.searchProfiler.advanceTimeDescription": "イテレーターを次のドキュメントに進めるためにかかった時間。", @@ -16894,7 +16893,7 @@ "xpack.security.management.editRole.updateRoleText": "ロールを更新", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} は数列でなければなりません", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名前は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は 1024 文字以内でなければなりません", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名前は {maxLength} 文字以内でなければなりません", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "権限が最低 1 つ必要です", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "スペースが最低 1 つ必要です", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "権限が必要です", @@ -17073,37 +17072,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "ユーザーを作成", "xpack.security.management.users.deleteUsersButtonLabel": "{numSelected} 人のユーザー{numSelected, plural, one { } other {s}} 削除", "xpack.security.management.users.deniedPermissionTitle": "ユーザーを管理するにはパーミッションが必要です", - "xpack.security.management.users.editUser.addRolesPlaceholder": "ロールを追加", - "xpack.security.management.users.editUser.cancelButtonLabel": "キャンセル", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "パスワードを変更", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "追加ステップが必要です", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "{username}ユーザーのパスワードを変更後、{kibana}ファイルを更新し、Kibanaを再起動する必要があります。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "ユーザー名は作成後変更できません。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "パスワードの確認", - "xpack.security.management.users.editUser.createUserButtonLabel": "ユーザーを作成", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "ユーザーを削除", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "このユーザーには非推奨ロールが割り当てられています。サポートされているロールに移行してください。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(非推奨)", - "xpack.security.management.users.editUser.editUserTitle": "ユーザー {userName} の編集", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "メールアドレス", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "ロールの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "ユーザーの読み込み中にエラーが発生", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "フルネーム", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "リザーブされたユーザーはビルトインのため削除または変更できません。パスワードのみ変更できます。", - "xpack.security.management.users.editUser.newUserTitle": "新規ユーザー", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "パスワードが一致しません", - "xpack.security.management.users.editUser.passwordFormRowLabel": "パスワード", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "パスワードは最低 6 文字必要です", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "ユーザー名が必要です", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "ユーザーリストに戻る", - "xpack.security.management.users.editUser.rolesFormRowLabel": "ロール", - "xpack.security.management.users.editUser.savingUserErrorMessage": "ユーザーの保存中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "パスワードの設定中にエラーが発生しました: {message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "ユーザーを更新", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "ユーザー名は文字またはアンダーラインで始まり、文字、アンダーライン、数字のみ使用できます", - "xpack.security.management.users.editUser.usernameFormRowLabel": "ユーザー名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "保存されたユーザー {message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "メールアドレスが無効です", "xpack.security.management.users.emailAddressColumnName": "メールアドレス", "xpack.security.management.users.extendedUserDeprecationNotice": "{username}ユーザーは推奨されません。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "ユーザーの取得中にエラーが発生: {message}", @@ -17140,7 +17108,6 @@ "xpack.security.roles.breadcrumb": "ロール", "xpack.security.roles.createBreadcrumb": "作成", "xpack.security.users.breadcrumb": "ユーザー", - "xpack.security.users.createBreadcrumb": "作成", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "プレス", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "値でフィルター", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "値を除外", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 51205a3420be5..08d064ce8a05c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -16675,7 +16675,6 @@ "xpack.savedObjectsTagging.uiApi.table.columnTagsName": "标签", "xpack.savedObjectsTagging.validation.color.errorInvalid": "标签颜色必须为有效的十六进制颜色", "xpack.savedObjectsTagging.validation.description.errorTooLong": "标签描述不能超过 {length} 个字符。", - "xpack.savedObjectsTagging.validation.name.errorInvalidCharacters": "标签名称只能包含 a-z、0-9、_、-、:。", "xpack.savedObjectsTagging.validation.name.errorTooLong": "标签名称不能超过 {length} 个字符", "xpack.savedObjectsTagging.validation.name.errorTooShort": "标签名称必须至少有 {length} 个字符", "xpack.searchProfiler.advanceTimeDescription": "将迭代器推进至下一文档所用时间。", @@ -16938,7 +16937,7 @@ "xpack.security.management.editRole.updateRoleText": "更新角色", "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为数组", "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", - "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 {maxLength} 个字符", "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "“权限”必填", @@ -17117,37 +17116,6 @@ "xpack.security.management.users.createNewUserButtonLabel": "创建用户", "xpack.security.management.users.deleteUsersButtonLabel": "删除 {numSelected} 个用户{numSelected, plural, one { } other { 个用户}}", "xpack.security.management.users.deniedPermissionTitle": "您需要用于管理用户的权限", - "xpack.security.management.users.editUser.addRolesPlaceholder": "添加角色", - "xpack.security.management.users.editUser.cancelButtonLabel": "取消", - "xpack.security.management.users.editUser.changePasswordButtonLabel": "更改密码", - "xpack.security.management.users.editUser.changePasswordExtraStepTitle": "需要额外的步骤", - "xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle": "更改 {username} 用户的密码后,必须更新 {kibana} 文件并重新启动 Kibana。", - "xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "用户名一经创建,将无法更改。", - "xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "确认密码", - "xpack.security.management.users.editUser.createUserButtonLabel": "创建用户", - "xpack.security.management.users.editUser.deleteUserButtonLabel": "删除用户", - "xpack.security.management.users.editUser.deprecatedRolesAssignedWarning": "为此用户分配了过时的角色。请迁移到支持的角色。", - "xpack.security.management.users.editUser.deprecatedRoleText": "(已过时)", - "xpack.security.management.users.editUser.editUserTitle": "编辑 {userName} 用户", - "xpack.security.management.users.editUser.emailAddressFormRowLabel": "电子邮件地址", - "xpack.security.management.users.editUser.errorLoadingRolesTitle": "加载角色时出错", - "xpack.security.management.users.editUser.errorLoadingUserTitle": "加载用户时出错", - "xpack.security.management.users.editUser.fullNameFormRowLabel": "全名", - "xpack.security.management.users.editUser.modifyingReservedUsersDescription": "保留用户是内置用户,无法删除或修改。只能更改密码。", - "xpack.security.management.users.editUser.newUserTitle": "新建用户", - "xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "密码不匹配", - "xpack.security.management.users.editUser.passwordFormRowLabel": "密码", - "xpack.security.management.users.editUser.passwordLengthErrorMessage": "密码长度必须至少为 6 个字符", - "xpack.security.management.users.editUser.requiredUsernameErrorMessage": "“用户名”必填", - "xpack.security.management.users.editUser.returnToUserListButtonLabel": "返回到用户列表", - "xpack.security.management.users.editUser.rolesFormRowLabel": "角色", - "xpack.security.management.users.editUser.savingUserErrorMessage": "保存用户时出错:{message}", - "xpack.security.management.users.editUser.settingPasswordErrorMessage": "设置密码时出错:{message}", - "xpack.security.management.users.editUser.updateUserButtonLabel": "更新用户", - "xpack.security.management.users.editUser.usernameAllowedCharactersErrorMessage": "用户名必须以字母或下划线开头,并只能包含字母、下划线和数字", - "xpack.security.management.users.editUser.usernameFormRowLabel": "用户名", - "xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage": "已保存用户{message}", - "xpack.security.management.users.editUser.validEmailRequiredErrorMessage": "电子邮件地址无效", "xpack.security.management.users.emailAddressColumnName": "电子邮件地址", "xpack.security.management.users.extendedUserDeprecationNotice": "用户 {username} 已过时。{reason}", "xpack.security.management.users.fetchingUsersErrorMessage": "提取用户时出错:{message}", @@ -17184,7 +17152,6 @@ "xpack.security.roles.breadcrumb": "角色", "xpack.security.roles.createBreadcrumb": "创建", "xpack.security.users.breadcrumb": "用户", - "xpack.security.users.createBreadcrumb": "创建", "xpack.securitySolution.accessibility.tooltipWithKeyboardShortcut.pressTooltipLabel": "按", "xpack.securitySolution.add_filter_to_global_search_bar.filterForValueHoverAction": "筛留值", "xpack.securitySolution.add_filter_to_global_search_bar.filterOutValueHoverAction": "筛除值", diff --git a/x-pack/plugins/uptime/common/runtime_types/network_events.ts b/x-pack/plugins/uptime/common/runtime_types/network_events.ts index 6104758f28fd8..fc666c803e2c3 100644 --- a/x-pack/plugins/uptime/common/runtime_types/network_events.ts +++ b/x-pack/plugins/uptime/common/runtime_types/network_events.ts @@ -41,6 +41,7 @@ export type NetworkEvent = t.TypeOf; export const SyntheticsNetworkEventsApiResponseType = t.type({ events: t.array(NetworkEventType), + total: t.number, }); export type SyntheticsNetworkEventsApiResponse = t.TypeOf< diff --git a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx index a8e4c90f2d29a..886496a7f6e2f 100644 --- a/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx +++ b/x-pack/plugins/uptime/public/components/common/step_detail_link.tsx @@ -5,8 +5,7 @@ */ import React, { FC } from 'react'; -import { EuiLink } from '@elastic/eui'; -import { Link } from 'react-router-dom'; +import { ReactRouterEuiButton } from './react_router_helpers'; interface StepDetailLinkProps { /** @@ -23,10 +22,14 @@ export const StepDetailLink: FC = ({ children, checkGroupId const to = `/journey/${checkGroupId}/step/${stepIndex}`; return ( - - - {children} - - + + {children} + ); }; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx index 01a599f8e8a60..934427643757d 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/executed_step.tsx @@ -13,6 +13,7 @@ import { StepScreenshotDisplay } from './step_screenshot_display'; import { StatusBadge } from './status_badge'; import { Ping } from '../../../../common/runtime_types'; import { StepDetailLink } from '../../common/step_detail_link'; +import { VIEW_PERFORMANCE } from './translations'; const CODE_BLOCK_OVERFLOW_HEIGHT = 360; @@ -26,24 +27,9 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) return ( <>
-
- {step.synthetics?.step?.index && checkGroup ? ( - - - - - - - - ) : ( - + + + = ({ step, index, checkGroup }) /> - )} -
- -
- -
+ +
+ +
+ +
@@ -73,6 +59,14 @@ export const ExecutedStep: FC = ({ step, index, checkGroup }) /> + {step.synthetics?.step?.index && ( + + + {VIEW_PERFORMANCE} + + + + )} { it('A colour palette comprising timing and mime type colours is correctly generated', () => { expect(colourPalette).toEqual({ - blocked: '#b9a888', + blocked: '#dcd4c4', connect: '#da8b45', dns: '#54b399', font: '#aa6556', @@ -173,10 +173,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, @@ -264,10 +264,10 @@ describe('getSeriesAndDomain', () => { }, Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 84.546ms", }, }, @@ -330,10 +330,10 @@ describe('getSeriesAndDomain', () => { "series": Array [ Object { "config": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "showTooltip": true, "tooltipProps": Object { - "colour": "#b9a888", + "colour": "#dcd4c4", "value": "Queued / Blocked: 0.854ms", }, }, diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts index 5e59026fd65f8..3cc0497bda8ec 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/data_formatting.ts @@ -197,7 +197,7 @@ const buildTimingPalette = (): TimingColourPalette => { const palette = Object.values(Timings).reduce>((acc, value) => { switch (value) { case Timings.Blocked: - acc[value] = SAFE_PALETTE[6]; + acc[value] = SAFE_PALETTE[16]; break; case Timings.Dns: acc[value] = SAFE_PALETTE[0]; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx index 7657ca7f9c64a..680e3f257841e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_container.tsx @@ -59,7 +59,10 @@ export const WaterfallChartContainer: React.FC = ({ checkGroup, stepIndex )} {networkEvents && !networkEvents.loading && networkEvents.events.length > 0 && ( - + )} ); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index a84765c4ea154..7b904511b58ab 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -48,10 +48,11 @@ export const renderLegendItem: RenderItem = (item) => { }; interface Props { + total: number; data: NetworkItems; } -export const WaterfallChartWrapper: React.FC = ({ data }) => { +export const WaterfallChartWrapper: React.FC = ({ data, total }) => { const [networkData] = useState(data); const { series, domain } = useMemo(() => { @@ -66,6 +67,8 @@ export const WaterfallChartWrapper: React.FC = ({ data }) => { return ( { + it('message in case total is greater than fetched', () => { + const { getByText, getByLabelText } = render( + + ); + + expect(getByText('First 1000/1100 network requests')).toBeInTheDocument(); + expect(getByLabelText('Info')).toBeInTheDocument(); + }); + + it('message in case total is equal to fetched requests', () => { + const { getByText } = render( + + ); + + expect(getByText('500 network requests')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx new file mode 100644 index 0000000000000..c54e32238f81c --- /dev/null +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/network_requests_total.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; +import { NetworkRequestsTotalStyle } from './styles'; + +interface Props { + totalNetworkRequests: number; + fetchedNetworkRequests: number; +} + +export const NetworkRequestsTotal = ({ totalNetworkRequests, fetchedNetworkRequests }: Props) => { + return ( + + + {i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage', { + defaultMessage: '{numNetworkRequests} network requests', + values: { + numNetworkRequests: + totalNetworkRequests > fetchedNetworkRequests + ? i18n.translate('xpack.uptime.synthetics.waterfall.requestsTotalMessage.first', { + defaultMessage: 'First {count}', + values: { count: `${fetchedNetworkRequests}/${totalNetworkRequests}` }, + }) + : totalNetworkRequests, + }, + })} + + {totalNetworkRequests > fetchedNetworkRequests && ( + + )} + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 1f70354db154e..7bf5100730f5e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { rgba } from 'polished'; import { euiStyled } from '../../../../../../../observability/public'; import { FIXED_AXIS_HEIGHT } from './constants'; @@ -103,3 +103,8 @@ export const WaterfallChartTooltip = euiStyled.div` color: ${(props) => props.theme.eui.euiColorLightestShade}; padding: ${(props) => props.theme.eui.paddingSizes.s}; `; + +export const NetworkRequestsTotalStyle = euiStyled(EuiText)` + line-height: ${FIXED_AXIS_HEIGHT}px; + margin-left: ${(props) => props.theme.eui.paddingSizes.m} +`; diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx index e937c3d35ec08..e449fed6decf4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_chart.tsx @@ -37,6 +37,7 @@ import { BAR_HEIGHT, CANVAS_MAX_ITEMS, MAIN_GROW_SIZE, SIDEBAR_GROW_SIZE } from import { Sidebar } from './sidebar'; import { Legend } from './legend'; import { useBarCharts } from './use_bar_charts'; +import { NetworkRequestsTotal } from './network_requests_total'; const Tooltip = (tooltipInfo: TooltipInfo) => { const { data, renderTooltipItem } = useWaterfallContext(); @@ -84,7 +85,13 @@ export const WaterfallChart = ({ maxHeight = '800px', fullHeight = false, }: WaterfallChartProps) => { - const { data, sidebarItems, legendItems } = useWaterfallContext(); + const { + data, + sidebarItems, + legendItems, + totalNetworkRequests, + fetchedNetworkRequests, + } = useWaterfallContext(); const [darkMode] = useUiSetting$('theme:darkMode'); @@ -115,7 +122,12 @@ export const WaterfallChart = ({ {shouldRenderSidebar && ( - + + + )} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx index ccee9d7994c80..4cf22f317bbd4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/context/waterfall_chart.tsx @@ -8,6 +8,8 @@ import React, { createContext, useContext, Context } from 'react'; import { WaterfallData, WaterfallDataEntry } from '../types'; export interface IWaterfallContext { + totalNetworkRequests: number; + fetchedNetworkRequests: number; data: WaterfallData; sidebarItems?: unknown[]; legendItems?: unknown[]; @@ -20,6 +22,8 @@ export interface IWaterfallContext { export const WaterfallContext = createContext>({}); interface ProviderProps { + totalNetworkRequests: number; + fetchedNetworkRequests: number; data: IWaterfallContext['data']; sidebarItems?: IWaterfallContext['sidebarItems']; legendItems?: IWaterfallContext['legendItems']; @@ -32,9 +36,20 @@ export const WaterfallProvider: React.FC = ({ sidebarItems, legendItems, renderTooltipItem, + totalNetworkRequests, + fetchedNetworkRequests, }) => { return ( - + {children} ); diff --git a/x-pack/plugins/uptime/public/state/reducers/network_events.ts b/x-pack/plugins/uptime/public/state/reducers/network_events.ts index 44a23b0fa53d7..666617f785182 100644 --- a/x-pack/plugins/uptime/public/state/reducers/network_events.ts +++ b/x-pack/plugins/uptime/public/state/reducers/network_events.ts @@ -18,6 +18,7 @@ export interface NetworkEventsState { [checkGroup: string]: { [stepIndex: number]: { events: NetworkEvent[]; + total: number; loading: boolean; error?: Error; }; @@ -45,16 +46,19 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: true, events: [], + total: 0, } : { loading: true, events: [], + total: 0, }, } : { [stepIndex]: { loading: true, events: [], + total: 0, }, }, }), @@ -62,7 +66,7 @@ export const networkEventsReducer = handleActions( [String(getNetworkEventsSuccess)]: ( state: NetworkEventsState, { - payload: { events, checkGroup, stepIndex }, + payload: { events, total, checkGroup, stepIndex }, }: Action ) => { return { @@ -74,16 +78,19 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: false, events, + total, } : { loading: false, events, + total, }, } : { [stepIndex]: { loading: false, events, + total, }, }, }; @@ -101,11 +108,13 @@ export const networkEventsReducer = handleActions( ...state[checkGroup][stepIndex], loading: false, events: [], + total: 0, error, } : { loading: false, events: [], + total: 0, error, }, } @@ -113,6 +122,7 @@ export const networkEventsReducer = handleActions( [stepIndex]: { loading: false, events: [], + total: 0, error, }, }, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts index e8618fabc4cca..2d590e80ca42d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.test.ts @@ -158,6 +158,7 @@ describe('getNetworkEvents', () => { esClient.search.mockResolvedValueOnce({ body: { hits: { + total: { value: 1 }, hits: mockHits, }, }, @@ -196,6 +197,7 @@ describe('getNetworkEvents', () => { }, }, "size": 1000, + "track_total_hits": true, }, "index": "heartbeat-8*", }, @@ -210,6 +212,7 @@ describe('getNetworkEvents', () => { esClient.search.mockResolvedValueOnce({ body: { hits: { + total: { value: 1 }, hits: mockHits, }, }, @@ -222,30 +225,33 @@ describe('getNetworkEvents', () => { }); expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "loadEndTime": 3287298.251, - "method": "GET", - "mimeType": "image/gif", - "requestSentTime": 3287154.973, - "requestStartTime": 3287155.502, - "status": 200, - "timestamp": "2020-12-14T10:46:39.183Z", - "timings": Object { - "blocked": 0.21400000014182297, - "connect": -1, - "dns": -1, - "proxy": -1, - "queueing": 0.5289999999149586, - "receive": 0.5340000002433953, - "send": 0.18799999998009298, - "ssl": -1, - "total": 143.27800000000934, - "wait": 141.81299999972907, + Object { + "events": Array [ + Object { + "loadEndTime": 3287298.251, + "method": "GET", + "mimeType": "image/gif", + "requestSentTime": 3287154.973, + "requestStartTime": 3287155.502, + "status": 200, + "timestamp": "2020-12-14T10:46:39.183Z", + "timings": Object { + "blocked": 0.21400000014182297, + "connect": -1, + "dns": -1, + "proxy": -1, + "queueing": 0.5289999999149586, + "receive": 0.5340000002433953, + "send": 0.18799999998009298, + "ssl": -1, + "total": 143.27800000000934, + "wait": 141.81299999972907, + }, + "url": "www.test.com", }, - "url": "www.test.com", - }, - ] + ], + "total": 1, + } `); }); }); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts index 1353175a8f94d..ec1fffd62350d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_network_events.ts @@ -14,9 +14,10 @@ interface GetNetworkEventsParams { export const getNetworkEvents: UMElasticsearchQueryFn< GetNetworkEventsParams, - NetworkEvent[] + { events: NetworkEvent[]; total: number } > = async ({ uptimeEsClient, checkGroup, stepIndex }) => { const params = { + track_total_hits: true, query: { bool: { filter: [ @@ -36,24 +37,28 @@ export const getNetworkEvents: UMElasticsearchQueryFn< const microToMillis = (micro: number): number => (micro === -1 ? -1 : micro * 1000); - return result.hits.hits.map((event: any) => { - const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); - const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); - const requestStartTime = - event._source.synthetics.payload.response && event._source.synthetics.payload.response.timing - ? microToMillis(event._source.synthetics.payload.response.timing.request_time) - : undefined; + return { + total: result.hits.total.value, + events: result.hits.hits.map((event: any) => { + const requestSentTime = microToMillis(event._source.synthetics.payload.request_sent_time); + const loadEndTime = microToMillis(event._source.synthetics.payload.load_end_time); + const requestStartTime = + event._source.synthetics.payload.response && + event._source.synthetics.payload.response.timing + ? microToMillis(event._source.synthetics.payload.response.timing.request_time) + : undefined; - return { - timestamp: event._source['@timestamp'], - method: event._source.synthetics.payload?.method, - url: event._source.synthetics.payload?.url, - status: event._source.synthetics.payload?.status, - mimeType: event._source.synthetics.payload?.response?.mime_type, - requestSentTime, - requestStartTime, - loadEndTime, - timings: event._source.synthetics.payload.timings, - }; - }); + return { + timestamp: event._source['@timestamp'], + method: event._source.synthetics.payload?.method, + url: event._source.synthetics.payload?.url, + status: event._source.synthetics.payload?.status, + mimeType: event._source.synthetics.payload?.response?.mime_type, + requestSentTime, + requestStartTime, + loadEndTime, + timings: event._source.synthetics.payload.timings, + }; + }), + }; }; diff --git a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts index f24b319baff00..7a6355ea4247d 100644 --- a/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts +++ b/x-pack/plugins/uptime/server/rest_api/network_events/get_network_events.ts @@ -26,8 +26,6 @@ export const createNetworkEventsRoute: UMRestApiRouteFactory = (libs: UMServerLi stepIndex, }); - return { - events: result, - }; + return result; }, }); diff --git a/x-pack/test/accessibility/apps/lens.ts b/x-pack/test/accessibility/apps/lens.ts index a7cacd0ad1cbb..71673d49c0c08 100644 --- a/x-pack/test/accessibility/apps/lens.ts +++ b/x-pack/test/accessibility/apps/lens.ts @@ -7,15 +7,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'visualize', 'header', 'home', 'settings', 'lens']); + const PageObjects = getPageObjects(['common', 'visualize', 'timePicker', 'home', 'lens']); const a11y = getService('a11y'); const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); - // FLAKY: https://github.com/elastic/kibana/issues/88926 - // FLAKY: https://github.com/elastic/kibana/issues/88927 - // FLAKY: https://github.com/elastic/kibana/issues/88929 - describe.skip('Lens', () => { + describe('Lens', () => { const lensChartName = 'MyLensChart'; before(async () => { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { @@ -35,12 +32,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('lens', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await a11y.testAppSnapshot(); }); it('lens XY chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.configureDimension({ dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension', @@ -75,6 +74,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('dimension configuration panel', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); + await PageObjects.timePicker.ensureHiddenNoDataPopover(); await PageObjects.lens.openDimensionEditor('lnsXY_xDimensionPanel > lns-empty-dimension'); await a11y.testAppSnapshot(); diff --git a/x-pack/test/accessibility/apps/users.ts b/x-pack/test/accessibility/apps/users.ts index efdcf4f3f022f..ede120ca43de7 100644 --- a/x-pack/test/accessibility/apps/users.ts +++ b/x-pack/test/accessibility/apps/users.ts @@ -13,6 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const a11y = getService('a11y'); const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); + const find = getService('find'); const retry = getService('retry'); describe('Kibana users page a11y tests', () => { @@ -52,24 +53,29 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('a11y test for roles drop down', async () => { - await testSubjects.setValue('userFormUserNameInput', 'a11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'a11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); + await PageObjects.security.clickElasticsearchUsers(); + await PageObjects.security.clickCreateNewUser(); + await PageObjects.security.fillUserForm({ + username: 'a11y', + password: 'password', + confirm_password: 'password', + full_name: 'a11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('rolesDropdown'); await a11y.testAppSnapshot(); }); - it('a11y test for display of delete button on users page ', async () => { - await testSubjects.setValue('userFormUserNameInput', 'deleteA11y'); - await testSubjects.setValue('passwordInput', 'password'); - await testSubjects.setValue('passwordConfirmationInput', 'password'); - await testSubjects.setValue('userFormFullNameInput', 'DeleteA11y user'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await testSubjects.click('rolesDropdown'); - await testSubjects.setValue('rolesDropdown', 'roleOption-apm_user'); - await testSubjects.click('userFormSaveButton'); + it('a11y test for display of delete button on users page', async () => { + await PageObjects.security.createUser({ + username: 'deleteA11y', + password: 'password', + confirm_password: 'password', + full_name: 'DeleteA11y user', + email: 'example@example.com', + roles: ['apm_user'], + }); await testSubjects.click('checkboxSelectRow-deleteA11y'); await a11y.testAppSnapshot(); }); @@ -77,17 +83,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('a11y test for delete user panel ', async () => { await testSubjects.click('deleteUserButton'); await a11y.testAppSnapshot(); + await testSubjects.click('confirmModalCancelButton'); }); it('a11y test for edit user panel', async () => { - await testSubjects.click('confirmModalCancelButton'); await PageObjects.settings.clickLinkText('deleteA11y'); await a11y.testAppSnapshot(); }); - it('a11y test for Change password screen', async () => { + it('a11y test for change password screen', async () => { + await PageObjects.settings.clickLinkText('deleteA11y'); + await find.clickByButtonText('Change password'); + await a11y.testAppSnapshot(); + await testSubjects.click('formFlyoutCancelButton'); + }); + + it('a11y test for deactivate user screen', async () => { await PageObjects.settings.clickLinkText('deleteA11y'); - await testSubjects.click('changePassword'); + await find.clickByButtonText('Deactivate user'); await a11y.testAppSnapshot(); }); }); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index 6b6326df017aa..2cd2654cffe3e 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -33,5 +33,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); + loadTestFile(require.resolve('./searchprofiler')); }); } diff --git a/x-pack/test/api_integration/apis/searchprofiler/index.ts b/x-pack/test/api_integration/apis/searchprofiler/index.ts new file mode 100644 index 0000000000000..36794feb00d1b --- /dev/null +++ b/x-pack/test/api_integration/apis/searchprofiler/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Search Profiler', () => { + loadTestFile(require.resolve('./searchprofiler')); + }); +} diff --git a/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts new file mode 100644 index 0000000000000..041cfb82520b4 --- /dev/null +++ b/x-pack/test/api_integration/apis/searchprofiler/searchprofiler.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +const API_BASE_PATH = '/api/searchprofiler'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('Profile', () => { + it('should return profile results for a valid index', async () => { + const payload = { + index: '_all', + query: { + query: { + match_all: {}, + }, + }, + }; + + const { body } = await supertest + .post(`${API_BASE_PATH}/profile`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(payload) + .expect(200); + + expect(body.ok).to.eql(true); + }); + + it('should return error for invalid index', async () => { + const payloadWithInvalidIndex = { + index: 'index_does_not_exist', + query: { + query: { + match_all: {}, + }, + }, + }; + + const { body } = await supertest + .post(`${API_BASE_PATH}/execute`) + .set('kbn-xsrf', 'xxx') + .set('Content-Type', 'application/json;charset=UTF-8') + .send(payloadWithInvalidIndex) + .expect(404); + + expect(body.error).to.eql('Not Found'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index 2d112215f4fc1..9084e635f8109 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges')); }); } diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index 191523e969717..6872f423fe630 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -19,6 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./change_password')); loadTestFile(require.resolve('./index_fields')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./privileges_basic')); }); } diff --git a/x-pack/test/api_integration/apis/security/users.ts b/x-pack/test/api_integration/apis/security/users.ts new file mode 100644 index 0000000000000..e177cf998beee --- /dev/null +++ b/x-pack/test/api_integration/apis/security/users.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const security = getService('security'); + const es = getService('es'); + + const mockUserName = 'test-user'; + const mockUserPassword = 'test-password'; + + describe('Users', () => { + beforeEach(async () => { + await security.user.create(mockUserName, { password: mockUserPassword, roles: [] }); + }); + + afterEach(async () => { + await security.user.delete(mockUserName); + }); + + it('should disable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_disable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(false); + }); + + it('should enable user', async () => { + await supertest + .post(`/internal/security/users/${mockUserName}/_enable`) + .set('kbn-xsrf', 'xxx') + .expect(204); + + const { body } = await es.security.getUser({ username: mockUserName }); + expect(body[mockUserName].enabled).to.be(true); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts new file mode 100644 index 0000000000000..8e9a01b28ea9b --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const es = getService('es'); + + describe('fleet_setup', () => { + skipIfNoDockerRegistry(providerContext); + beforeEach(async () => { + try { + await es.security.deleteUser({ + username: 'fleet_enroll', + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + try { + await es.security.deleteRole({ + name: 'fleet_enroll', + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + }); + + it('should not create a fleet_enroll role if one does not already exist', async () => { + const { body: apiResponse } = await supertest + .post(`/api/fleet/setup`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(apiResponse.isInitialized).to.be(true); + + try { + await es.security.getUser({ + username: 'fleet_enroll', + }); + } catch (e) { + expect(e.meta?.statusCode).to.eql(404); + } + }); + + it('should update the fleet_enroll role with new index permissions if one does already exist', async () => { + try { + await es.security.putRole({ + name: 'fleet_enroll', + body: { + cluster: ['monitor', 'manage_api_key'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + ], + privileges: ['write', 'create_index', 'indices:admin/auto_create'], + allow_restricted_indices: false, + }, + ], + applications: [], + run_as: [], + metadata: {}, + transient_metadata: { enabled: true }, + }, + }); + } catch (e) { + if (e.meta?.statusCode !== 404) { + throw e; + } + } + + const { body: apiResponse } = await supertest + .post(`/api/fleet/setup`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + expect(apiResponse.isInitialized).to.be(true); + + const { body: roleResponse } = await es.security.getRole({ + name: 'fleet_enroll', + }); + expect(roleResponse).to.have.key('fleet_enroll'); + expect(roleResponse.fleet_enroll).to.eql({ + cluster: ['monitor', 'manage_api_key'], + indices: [ + { + names: [ + 'logs-*', + 'metrics-*', + 'traces-*', + '.ds-logs-*', + '.ds-metrics-*', + '.ds-traces-*', + '.logs-endpoint.diagnostic.collection-*', + '.ds-.logs-endpoint.diagnostic.collection-*', + ], + privileges: ['write', 'create_index', 'indices:admin/auto_create'], + allow_restricted_indices: false, + }, + ], + applications: [], + run_as: [], + metadata: {}, + transient_metadata: { enabled: true }, + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index 0d634f60e282f..f472599652224 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -7,6 +7,8 @@ export default function ({ loadTestFile }) { describe('Fleet Endpoints', function () { this.tags('ciGroup10'); + // Fleet setup + loadTestFile(require.resolve('./fleet_setup')); // Agent setup loadTestFile(require.resolve('./agents_setup')); // Agents diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 00183113a4d59..b2ddf7d47b1f1 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -68,46 +68,36 @@ export default function ({ getService, getPageObjects }) { before('Create dashboard only mode user', async () => { await PageObjects.settings.navigateTo(); - await PageObjects.security.clickUsersSection(); - await PageObjects.security.clickCreateNewUser(); - await testSubjects.setValue('userFormUserNameInput', 'dashuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'dashuser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'dashuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'dashuser', + roles: ['kibana_dashboard_only_user', 'logstash-data'], + }); }); before('Create user with mixes roles', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mixeduser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('kibana_admin'); - await PageObjects.security.assignRoleToUser('logstash-data'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mixeduser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'kibana_admin', 'logstash-data'], + }); }); before('Create user with dashboard and superuser role', async () => { - await PageObjects.security.clickCreateNewUser(); - - await testSubjects.setValue('userFormUserNameInput', 'mysuperuser'); - await testSubjects.setValue('passwordInput', '123456'); - await testSubjects.setValue('passwordConfirmationInput', '123456'); - await testSubjects.setValue('userFormFullNameInput', 'mixeduser'); - await testSubjects.setValue('userFormEmailInput', 'example@example.com'); - await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user'); - await PageObjects.security.assignRoleToUser('superuser'); - - await PageObjects.security.clickSaveEditUser(); + await PageObjects.security.createUser({ + username: 'mysuperuser', + password: '123456', + confirm_password: '123456', + email: 'example@example.com', + full_name: 'mixeduser', + roles: ['kibana_dashboard_only_user', 'superuser'], + }); }); after(async () => { diff --git a/x-pack/test/functional/apps/lens/drag_and_drop.ts b/x-pack/test/functional/apps/lens/drag_and_drop.ts index e0130bc394271..57e5990a74012 100644 --- a/x-pack/test/functional/apps/lens/drag_and_drop.ts +++ b/x-pack/test/functional/apps/lens/drag_and_drop.ts @@ -11,73 +11,108 @@ export default function ({ getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']); describe('lens drag and drop tests', () => { - it('should construct the basic split xy chart', async () => { - await PageObjects.visualize.navigateToNewVisualization(); - await PageObjects.visualize.clickVisType('lens'); - await PageObjects.lens.goToTimeRange(); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + describe('basic drag and drop', () => { + it('should construct the basic split xy chart', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); - expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( - '@timestamp' - ); - }); + expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql( + '@timestamp' + ); + }); - it('should allow dropping fields to existing and empty dimension triggers', async () => { - await PageObjects.lens.switchToVisualization('lnsDatatable'); + it('should allow dropping fields to existing and empty dimension triggers', async () => { + await PageObjects.lens.switchToVisualization('lnsDatatable'); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'clientip', - 'lnsDatatable_column > lns-dimensionTrigger' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( - 'Top values of clientip' - ); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'clientip', + 'lnsDatatable_column > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql( + 'Top values of clientip' + ); - await PageObjects.lens.dragFieldToDimensionTrigger( - 'bytes', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( - 'bytes' - ); - await PageObjects.lens.dragFieldToDimensionTrigger( - '@message.raw', - 'lnsDatatable_column > lns-empty-dimension' - ); - expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( - 'Top values of @message.raw' - ); - }); + await PageObjects.lens.dragFieldToDimensionTrigger( + 'bytes', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql( + 'bytes' + ); + await PageObjects.lens.dragFieldToDimensionTrigger( + '@message.raw', + 'lnsDatatable_column > lns-empty-dimension' + ); + expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql( + 'Top values of @message.raw' + ); + }); - it('should reorder the elements for the table', async () => { - await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); - await PageObjects.header.waitUntilLoadingHasFinished(); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ - 'Top values of @message.raw', - 'Top values of clientip', - 'bytes', - ]); - }); + it('should reorder the elements for the table', async () => { + await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([ + 'Top values of @message.raw', + 'Top values of clientip', + 'bytes', + ]); + }); - it('should move the column to compatible dimension group', async () => { - await PageObjects.lens.switchToVisualization('bar'); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of clientip', - ]); + it('should move the column to compatible dimension group', async () => { + await PageObjects.lens.switchToVisualization('bar'); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'Top values of @message.raw', + ]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + + await PageObjects.lens.dragDimensionToDimension( + 'lnsXY_xDimensionPanel > lns-dimensionTrigger', + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql( + [] + ); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of @message.raw']); + }); + }); - await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_xDimensionPanel > lns-dimensionTrigger', - 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' - ); + describe('workspace drop', () => { + it('should always nest time dimension in categorical dimension', async () => { + await PageObjects.visualize.navigateToNewVisualization(); + await PageObjects.visualize.clickVisType('lens'); + await PageObjects.lens.goToTimeRange(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('@timestamp'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel') + ).to.eql(['Top values of clientip']); + await PageObjects.lens.openDimensionEditor( + 'lnsXY_splitDimensionPanel > lns-dimensionTrigger' + ); + expect(await PageObjects.lens.isTopLevelAggregation()).to.be(true); + await PageObjects.lens.closeDimensionEditor(); + }); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([]); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([ - 'Top values of @message.raw', - ]); + it('overwrite existing time dimension if one exists already', async () => { + await PageObjects.lens.dragFieldToWorkspace('utc_time'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.dragFieldToWorkspace('clientip'); + await PageObjects.header.waitUntilLoadingHasFinished(); + expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([ + 'utc_time', + ]); + }); }); }); } diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 0595322ad2d21..a76475fbbbd8c 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -51,14 +51,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user userEAST ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'userEast', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'dls EAST', + confirm_password: 'changeme', + full_name: 'dls EAST', email: 'dlstest@elastic.com', - save: true, roles: ['kibana_admin', 'myroleEast'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 3f3984dd05a94..a4e2680c394ee 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -71,14 +71,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer1 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer1', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer one', + confirm_password: 'changeme', + full_name: 'customer one', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_viewssnrole'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -87,14 +85,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user customer2 ', async function () { - await PageObjects.security.clickElasticsearchUsers(); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'customer2', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'customer two', + confirm_password: 'changeme', + full_name: 'customer two', email: 'flstest@elastic.com', - save: true, roles: ['kibana_admin', 'a_view_no_ssn_role'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index 58c72eaa3072e..de4515c501187 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -58,13 +58,12 @@ export default function ({ getService, getPageObjects }) { ], }, }); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanauser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanafirst kibanalast', + confirm_password: 'changeme', + full_name: 'kibanafirst kibanalast', email: 'kibanauser@myEmail.com', save: true, roles: ['rbac_all'], @@ -76,13 +75,12 @@ export default function ({ getService, getPageObjects }) { expect(users.kibanauser.roles).to.eql(['rbac_all']); expect(users.kibanauser.fullname).to.eql('kibanafirst kibanalast'); expect(users.kibanauser.reserved).to.be(false); - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'kibanareadonly', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'kibanareadonlyFirst kibanareadonlyLast', + confirm_password: 'changeme', + full_name: 'kibanareadonlyFirst kibanareadonlyLast', email: 'kibanareadonly@myEmail.com', save: true, roles: ['rbac_read'], diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 96f16aebd11b9..6f76367801536 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -9,7 +9,7 @@ import { parse } from 'url'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - const pageObjects = getPageObjects(['common', 'roleMappings']); + const pageObjects = getPageObjects(['common', 'security', 'roleMappings']); const security = getService('security'); const testSubjects = getService('testSubjects'); const browser = getService('browser'); @@ -32,8 +32,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('allows a role mapping to be created', async () => { await testSubjects.click('createRoleMappingButton'); await testSubjects.setValue('roleMappingFormNameInput', 'new_role_mapping'); - await testSubjects.setValue('rolesDropdown', 'superuser'); - await browser.pressKeys(browser.keys.ENTER); + await pageObjects.security.selectRole('superuser'); await testSubjects.click('roleMappingsAddRuleButton'); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index c547657bf880a..830d8384f1e3d 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -51,15 +51,13 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.clickElasticsearchUsers(); log.debug('After Add user new: , userObj.userName'); - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Rashmi', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'RashmiFirst RashmiLast', + confirm_password: 'changeme', + full_name: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', - save: true, roles: ['logstash_reader', 'kibana_admin'], }); log.debug('After Add user: , userObj.userName'); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index a2a2b705172d7..c05220b6a59f3 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -19,13 +19,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'newuser', password: 'changeme', - confirmPassword: 'changeme', - fullname: 'newuserFirst newuserLast', + confirm_password: 'changeme', + full_name: 'newuserFirst newuserLast', email: 'newuser@myEmail.com', - save: true, roles: ['kibana_admin', 'superuser'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index 4fd4384a93c59..7f2b0cfd96ca2 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -41,13 +41,12 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'Lee', password: 'LeePwd', - confirmPassword: 'LeePwd', - fullname: 'LeeFirst LeeLast', + confirm_password: 'LeePwd', + full_name: 'LeeFirst LeeLast', email: 'lee@myEmail.com', - save: true, roles: ['kibana_admin'], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); @@ -59,11 +58,10 @@ export default function ({ getService, getPageObjects }) { }); it('should add new user with optional fields left empty', async function () { - await PageObjects.security.addUser({ + await PageObjects.security.createUser({ username: 'OptionalUser', password: 'OptionalUserPwd', - confirmPassword: 'OptionalUserPwd', - save: true, + confirm_password: 'OptionalUserPwd', roles: [], }); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); diff --git a/x-pack/test/functional/es_archives/lens/basic/data.json b/x-pack/test/functional/es_archives/lens/basic/data.json new file mode 100644 index 0000000000000..a985de882929d --- /dev/null +++ b/x-pack/test/functional/es_archives/lens/basic/data.json @@ -0,0 +1,577 @@ +{ + "type": "doc", + "value": { + "id": "space:default", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "space": "6.6.0" + }, + "references": [], + "space": { + "_reserved": true, + "description": "This is the default space!", + "disabledFeatures": [], + "name": "Default" + }, + "type": "space" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "index-pattern:log*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "log*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + + +{ + "type": "doc", + "value": { + "id": "custom_space:index-pattern:logstash-*", + "index": ".kibana_1", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]", + "timeFieldName": "@timestamp", + "title": "logstash-*" + }, + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "namespace": "custom_space", + "references": [], + "type": "index-pattern", + "updated_at": "2018-12-21T00:43:07.096Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "custom_space:visualization:i-exist", + "index": ".kibana_1", + "source": { + "migrationVersion": { + "visualization": "7.3.1" + }, + "namespace": "custom_space", + "references": [ + { + "id": "logstash-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + } + ], + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z", + "visualization": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[],\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\"}" + }, + "title": "A Pie", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}" + } + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "query:okjpgs", + "index": ".kibana_1", + "source": { + "query": { + "description": "Ok responses for jpg files", + "filters": [ + { + "$state": { + "store": "appState" + }, + "meta": { + "alias": null, + "disabled": false, + "index": "b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b", + "key": "extension.raw", + "negate": false, + "params": { + "query": "jpg" + }, + "type": "phrase", + "value": "jpg" + }, + "query": { + "match": { + "extension.raw": { + "query": "jpg", + "type": "phrase" + } + } + } + } + ], + "query": { + "language": "kuery", + "query": "response:200" + }, + "title": "OKJpgs" + }, + "references": [], + "type": "query", + "updated_at": "2019-07-17T17:54:26.378Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "config:8.0.0", + "index": ".kibana_1", + "source": { + "config": { + "accessibility:disableAnimations": true, + "buildNum": 9007199254740991, + "dateFormat:tz": "UTC", + "defaultIndex": "logstash-*" + }, + "references": [], + "type": "config", + "updated_at": "2019-09-04T18:47:24.761Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac1", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"c61a8afb-a185-4fae-a064-fb3846f6c451\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"max\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\"}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-2cd09808-3915-49f4-b3b0-82767eba23f7\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Maximum of bytes\\\",\\\"operationType\\\":\\\"max\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"2cd09808-3915-49f4-b3b0-82767eba23f7\\\"}}\"}\n| lens_metric_chart title=\"Maximum of bytes\" accessor=\"2cd09808-3915-49f4-b3b0-82767eba23f7\" mode=\"full\"", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "c61a8afb-a185-4fae-a064-fb3846f6c451": { + "columnOrder": [ + "2cd09808-3915-49f4-b3b0-82767eba23f7" + ], + "columns": { + "2cd09808-3915-49f4-b3b0-82767eba23f7": { + "dataType": "number", + "isBucketed": false, + "label": "Maximum of bytes", + "operationType": "max", + "scale": "ratio", + "sourceField": "bytes" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "accessor": "2cd09808-3915-49f4-b3b0-82767eba23f7", + "isHorizontal": false, + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "layers": [ + { + "accessors": [ + "d3e62a7a-c259-4fff-a2fc-eebf20b7008a", + "26ef70a9-c837-444c-886e-6bd905ee7335" + ], + "layerId": "c61a8afb-a185-4fae-a064-fb3846f6c451", + "seriesType": "area", + "splitAccessor": "54cd64ed-2a44-4591-af84-b2624504569a", + "xAccessor": "d6e40cea-6299-43b4-9c9d-b4ee305a2ce8" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "area" + } + }, + "title": "Artistpreviouslyknownaslens", + "visualizationType": "lnsMetric" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:9536bed0-d57e-11ea-b169-e3a222a76b9c", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"language\\\":\\\"kuery\\\",\\\"query\\\":\\\"\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=true partialRows=true includeFormatHints=true aggConfigs=\"[{\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.dest\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":7,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"geo.src\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\" | lens_rename_columns idMap=\"{\\\"col-0-bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\":{\\\"dataType\\\":\\\"string\\\",\\\"isBucketed\\\":true,\\\"label\\\":\\\"Top values of geo.dest\\\",\\\"operationType\\\":\\\"terms\\\",\\\"params\\\":{\\\"orderBy\\\":{\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"type\\\":\\\"column\\\"},\\\"orderDirection\\\":\\\"desc\\\",\\\"size\\\":7},\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.dest\\\",\\\"id\\\":\\\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\\\"},\\\"col-2-c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\":{\\\"label\\\":\\\"Top values of geo.src\\\",\\\"dataType\\\":\\\"string\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"sourceField\\\":\\\"geo.src\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\\\"},\\\"col-3-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"dataType\\\":\\\"number\\\",\\\"isBucketed\\\":false,\\\"label\\\":\\\"Average of bytes\\\",\\\"operationType\\\":\\\"avg\\\",\\\"scale\\\":\\\"ratio\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_pie shape=\"pie\" hideLabels=false groups=\"bafe3009-1776-4227-a0fe-b0d6ccbb4961\"\n groups=\"c1ebe4c9-f283-486c-ae95-6b3e99e83bd8\" metric=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" numberDisplay=\"percent\" categoryDisplay=\"default\" legendDisplay=\"default\" legendPosition=\"right\" percentDecimals=3 nestedLegend=false", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "5bd1c078-e1dd-465b-8d25-7a6404befa88": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp" + }, + "65340cf3-8402-4494-96f2-293701c59571": { + "dataType": "number", + "isBucketed": true, + "label": "Top values of bytes", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "bytes" + }, + "87554e1d-3dbf-4c1c-a358-4c9d40424cfa": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of type", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "type" + }, + "bafe3009-1776-4227-a0fe-b0d6ccbb4961": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.dest", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 7 + }, + "scale": "ordinal", + "sourceField": "geo.dest" + }, + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8": { + "dataType": "string", + "isBucketed": true, + "label": "Top values of geo.src", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "geo.src" + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "categoryDisplay": "default", + "groups": [ + "bafe3009-1776-4227-a0fe-b0d6ccbb4961", + "c1ebe4c9-f283-486c-ae95-6b3e99e83bd8" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "legendDisplay": "default", + "metric": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "nestedLegend": false, + "numberDisplay": "percent" + } + ], + "shape": "pie" + } + }, + "title": "lnsPieVis", + "visualizationType": "lnsPie" + }, + "references": [], + "type": "lens", + "updated_at": "2020-08-03T11:43:43.421Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "lens:76fc4200-cf44-11e9-b933-fd84270f3ac2", + "index": ".kibana_1", + "source": { + "lens": { + "expression": "kibana\n| kibana_context query=\"{\\\"query\\\":\\\"\\\",\\\"language\\\":\\\"kuery\\\"}\" filters=\"[]\"\n| lens_merge_tables layerIds=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" \n tables={esaggs index=\"logstash-*\" metricsAtAllLevels=false partialRows=false includeFormatHints=true aggConfigs={lens_auto_date aggConfigs=\"[{\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"terms\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"ip\\\",\\\"orderBy\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"order\\\":\\\"desc\\\",\\\"size\\\":3,\\\"otherBucket\\\":false,\\\"otherBucketLabel\\\":\\\"Other\\\",\\\"missingBucket\\\":false,\\\"missingBucketLabel\\\":\\\"Missing\\\"}},{\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"date_histogram\\\",\\\"schema\\\":\\\"segment\\\",\\\"params\\\":{\\\"field\\\":\\\"@timestamp\\\",\\\"useNormalizedEsInterval\\\":true,\\\"interval\\\":\\\"auto\\\",\\\"drop_partials\\\":false,\\\"min_doc_count\\\":0,\\\"extended_bounds\\\":{}}},{\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\",\\\"enabled\\\":true,\\\"type\\\":\\\"avg\\\",\\\"schema\\\":\\\"metric\\\",\\\"params\\\":{\\\"field\\\":\\\"bytes\\\",\\\"missing\\\":0}}]\"} | lens_rename_columns idMap=\"{\\\"col-0-7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":{\\\"label\\\":\\\"Top values of ip\\\",\\\"dataType\\\":\\\"ip\\\",\\\"operationType\\\":\\\"terms\\\",\\\"scale\\\":\\\"ordinal\\\",\\\"suggestedPriority\\\":0,\\\"sourceField\\\":\\\"ip\\\",\\\"isBucketed\\\":true,\\\"params\\\":{\\\"size\\\":3,\\\"orderBy\\\":{\\\"type\\\":\\\"column\\\",\\\"columnId\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"},\\\"orderDirection\\\":\\\"desc\\\"},\\\"id\\\":\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\"},\\\"col-1-3cf18f28-3495-4d45-a55f-d97f88022099\\\":{\\\"label\\\":\\\"@timestamp\\\",\\\"dataType\\\":\\\"date\\\",\\\"operationType\\\":\\\"date_histogram\\\",\\\"suggestedPriority\\\":1,\\\"sourceField\\\":\\\"@timestamp\\\",\\\"isBucketed\\\":true,\\\"scale\\\":\\\"interval\\\",\\\"params\\\":{\\\"interval\\\":\\\"auto\\\"},\\\"id\\\":\\\"3cf18f28-3495-4d45-a55f-d97f88022099\\\"},\\\"col-2-3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":{\\\"label\\\":\\\"Average of bytes\\\",\\\"dataType\\\":\\\"number\\\",\\\"operationType\\\":\\\"avg\\\",\\\"sourceField\\\":\\\"bytes\\\",\\\"isBucketed\\\":false,\\\"scale\\\":\\\"ratio\\\",\\\"id\\\":\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\"}}\"}\n| lens_xy_chart xTitle=\"@timestamp\" yTitle=\"Average of bytes\" legend={lens_xy_legendConfig isVisible=true position=\"right\"} \n layers={lens_xy_layer layerId=\"4ba1a1be-6e67-434b-b3a0-f30db8ea5395\" hide=false xAccessor=\"3cf18f28-3495-4d45-a55f-d97f88022099\" yScaleType=\"linear\" xScaleType=\"time\" isHistogram=true splitAccessor=\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\" seriesType=\"bar_stacked\" accessors=\"3dc0bd55-2087-4e60-aea2-f9910714f7db\" columnToLabel=\"{\\\"3dc0bd55-2087-4e60-aea2-f9910714f7db\\\":\\\"Average of bytes\\\",\\\"7a5d833b-ca6f-4e48-a924-d2a28d365dc3\\\":\\\"Top values of ip\\\"}\"}", + "state": { + "datasourceMetaData": { + "filterableIndexPatterns": [ + { + "id": "logstash-*", + "title": "logstash-*" + } + ] + }, + "datasourceStates": { + "indexpattern": { + "currentIndexPatternId": "logstash-*", + "layers": { + "4ba1a1be-6e67-434b-b3a0-f30db8ea5395": { + "columnOrder": [ + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "3cf18f28-3495-4d45-a55f-d97f88022099", + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "columns": { + "3cf18f28-3495-4d45-a55f-d97f88022099": { + "dataType": "date", + "isBucketed": true, + "label": "@timestamp", + "operationType": "date_histogram", + "params": { + "interval": "auto" + }, + "scale": "interval", + "sourceField": "@timestamp", + "suggestedPriority": 1 + }, + "3dc0bd55-2087-4e60-aea2-f9910714f7db": { + "dataType": "number", + "isBucketed": false, + "label": "Average of bytes", + "operationType": "avg", + "scale": "ratio", + "sourceField": "bytes" + }, + "7a5d833b-ca6f-4e48-a924-d2a28d365dc3": { + "dataType": "ip", + "isBucketed": true, + "label": "Top values of ip", + "operationType": "terms", + "params": { + "orderBy": { + "columnId": "3dc0bd55-2087-4e60-aea2-f9910714f7db", + "type": "column" + }, + "orderDirection": "desc", + "size": 3 + }, + "scale": "ordinal", + "sourceField": "ip", + "suggestedPriority": 0 + } + }, + "indexPatternId": "logstash-*" + } + } + } + }, + "filters": [], + "query": { + "language": "kuery", + "query": "" + }, + "visualization": { + "layers": [ + { + "accessors": [ + "3dc0bd55-2087-4e60-aea2-f9910714f7db" + ], + "layerId": "4ba1a1be-6e67-434b-b3a0-f30db8ea5395", + "seriesType": "bar_stacked", + "splitAccessor": "7a5d833b-ca6f-4e48-a924-d2a28d365dc3", + "xAccessor": "3cf18f28-3495-4d45-a55f-d97f88022099" + } + ], + "legend": { + "isVisible": true, + "position": "right" + }, + "preferredSeriesType": "bar_stacked" + } + }, + "title": "lnsXYvis", + "visualizationType": "lnsXY" + }, + "references": [], + "type": "lens", + "updated_at": "2019-10-16T00:28:08.979Z" + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "ui-metric:DashboardPanelVersionInUrl:8.0.0", + "index": ".kibana_1", + "source": { + "type": "ui-metric", + "ui-metric": { + "count": 1 + }, + "updated_at": "2019-10-16T00:28:24.399Z" + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/page_objects/graph_page.ts b/x-pack/test/functional/page_objects/graph_page.ts index 9ce1f87b5bf3d..8dda82e4e08bb 100644 --- a/x-pack/test/functional/page_objects/graph_page.ts +++ b/x-pack/test/functional/page_objects/graph_page.ts @@ -196,7 +196,7 @@ export function GraphPageProvider({ getService, getPageObjects }: FtrProviderCon await testSubjects.click('confirmSaveSavedObjectButton'); // Confirm that the Graph has been saved. - return await testSubjects.exists('saveGraphSuccess'); + return await testSubjects.exists('saveGraphSuccess', { timeout: 10000 }); } async getSearchFilter() { diff --git a/x-pack/test/functional/page_objects/lens_page.ts b/x-pack/test/functional/page_objects/lens_page.ts index 04c660847bcee..31a4d6e29fc35 100644 --- a/x-pack/test/functional/page_objects/lens_page.ts +++ b/x-pack/test/functional/page_objects/lens_page.ts @@ -241,6 +241,9 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont }); }, + async isTopLevelAggregation() { + return await testSubjects.isEuiSwitchChecked('indexPattern-nesting-switch'); + }, /** * Removes the dimension matching a specific test subject */ diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index cad5e29528e9c..868d8115e7f0f 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -7,6 +7,7 @@ import { adminTestUser } from '@kbn/test'; import { FtrProviderContext } from '../ftr_provider_context'; import { AuthenticatedUser, Role } from '../../../plugins/security/common/model'; +import type { UserFormValues } from '../../../plugins/security/public/management/users/edit_user/user_form'; export function SecurityPageProvider({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); @@ -275,7 +276,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickCancelEditUser() { - await testSubjects.click('userFormCancelButton'); + await find.clickByButtonText('Cancel'); } async clickCancelEditRole() { @@ -283,7 +284,7 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider } async clickSaveEditUser() { - await testSubjects.click('userFormSaveButton'); + await find.clickByButtonText('Update user'); await PageObjects.header.waitUntilLoadingHasFinished(); } @@ -380,53 +381,58 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider return roles; } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewUser() { return await testSubjects.click('createUserButton'); } + /** + * @deprecated Use `PageObjects.security.clickCreateNewUser` instead + */ async clickNewRole() { return await testSubjects.click('createRoleButton'); } - async addUser(userObj: { - username: string; - password: string; - confirmPassword: string; - email: string; - fullname: string; - roles: string[]; - save?: boolean; - }) { - const self = this; - await this.clickNewUser(); - log.debug('username = ' + userObj.username); - await testSubjects.setValue('userFormUserNameInput', userObj.username); - await testSubjects.setValue('passwordInput', userObj.password); - await testSubjects.setValue('passwordConfirmationInput', userObj.confirmPassword); - if (userObj.fullname) { - await testSubjects.setValue('userFormFullNameInput', userObj.fullname); + async fillUserForm(user: UserFormValues) { + if (user.username) { + await find.setValue('[name=username]', user.username); + } + if (user.password) { + await find.setValue('[name=password]', user.password); + } + if (user.confirm_password) { + await find.setValue('[name=confirm_password]', user.confirm_password); } - if (userObj.email) { - await testSubjects.setValue('userFormEmailInput', userObj.email); + if (user.full_name) { + await find.setValue('[name=full_name]', user.full_name); + } + if (user.email) { + await find.setValue('[name=email]', user.email); } - log.debug('Add roles: ', userObj.roles); - const rolesToAdd = userObj.roles || []; + const rolesToAdd = user.roles || []; for (let i = 0; i < rolesToAdd.length; i++) { - await self.selectRole(rolesToAdd[i]); - } - log.debug('After Add role: , userObj.roleName'); - if (userObj.save === true) { - await testSubjects.click('userFormSaveButton'); - } else { - await testSubjects.click('userFormCancelButton'); + await this.selectRole(rolesToAdd[i]); } } + async submitCreateUserForm() { + await find.clickByButtonText('Create user'); + } + + async createUser(user: UserFormValues) { + await this.clickElasticsearchUsers(); + await this.clickCreateNewUser(); + await this.fillUserForm(user); + await this.submitCreateUserForm(); + } + async addRole(roleName: string, roleObj: Role) { const self = this; - await this.clickNewRole(); + await this.clickCreateNewRole(); // We have to use non-test-subject selectors because this markup is generated by ui-select. log.debug('roleObj.indices[0].names = ' + roleObj.elasticsearch.indices[0].names); @@ -498,37 +504,23 @@ export function SecurityPageProvider({ getService, getPageObjects }: FtrProvider const dropdown = await testSubjects.find('rolesDropdown'); const input = await dropdown.findByCssSelector('input'); await input.type(role); - await testSubjects.click(`roleOption-${role}`); + await find.clickByCssSelector(`[role=option][title="${role}"]`); await testSubjects.click('comboBoxToggleListButton'); - await testSubjects.find(`roleOption-${role}`); } - deleteUser(username: string) { - let alertText: string; + async deleteUser(username: string) { log.debug('Delete user ' + username); - return find - .clickByDisplayedLinkText(username) - .then(() => { - return PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - }) - .then(() => { - log.debug('Find delete button and click'); - return testSubjects.click('userFormDeleteButton'); - }) - .then(() => { - return PageObjects.common.sleep(2000); - }) - .then(() => { - return testSubjects.getVisibleText('confirmModalBodyText'); - }) - .then((alert) => { - alertText = alert; - log.debug('Delete user alert text = ' + alertText); - return testSubjects.click('confirmModalConfirmButton'); - }) - .then(() => { - return alertText; - }); + await find.clickByDisplayedLinkText(username); + await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + + log.debug('Find delete button and click'); + await find.clickByButtonText('Delete user'); + await PageObjects.common.sleep(2000); + + const confirmText = await testSubjects.getVisibleText('confirmModalBodyText'); + log.debug('Delete user alert text = ' + confirmText); + await testSubjects.click('confirmModalConfirmButton'); + return confirmText; } } return new SecurityPage(); diff --git a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json index 69220756639dc..8379290f5d9bb 100644 --- a/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json +++ b/x-pack/test/plugin_functional/es_archives/global_search/search_syntax/data.json @@ -88,6 +88,24 @@ } } +{ + "type": "doc", + "value": { + "id": "tag:tag-special-chars", + "index": ".kibana", + "source": { + "tag": { + "name": "my%tag", + "description": "Special chars", + "color": "#AA0077" + }, + "type": "tag", + "updated_at": "2017-09-21T18:49:16.270Z" + }, + "type": "doc" + } +} + { "type": "doc", "value": { @@ -356,3 +374,41 @@ } } } + +{ + "type": "doc", + "value": { + "id": "dashboard:ref-to-tag-special-chars", + "index": ".kibana", + "source": { + "dashboard": { + "title": "dashboard 4 (tag-special-chars)", + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[{\"meta\":{\"negate\":false,\"disabled\":false,\"alias\":null,\"type\":\"phrase\",\"key\":\"animal\",\"value\":\"dog\",\"params\":{\"query\":\"dog\",\"type\":\"phrase\"},\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\"},\"query\":{\"match\":{\"animal\":{\"query\":\"dog\",\"type\":\"phrase\"}}},\"$state\":{\"store\":\"appState\"}}],\"highlightAll\":true,\"version\":true}" + }, + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "panelsJSON": "[]", + "timeFrom": "Mon Apr 09 2018 17:56:08 GMT-0400", + "timeRestore": true, + "timeTo": "Wed Apr 11 2018 17:56:08 GMT-0400", + "version": 1 + }, + "migrationVersion": { + "dashboard": "7.3.0" + }, + "references": [ + { + "id": "tag-special-chars", + "name": "tag-special-ref", + "type": "tag" + } + ], + "type": "dashboard", + "updated_at": "2018-04-11T21:57:52.253Z" + } + } +} + + diff --git a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts index f0c70ee8f718d..6f84440fc27e6 100644 --- a/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts +++ b/x-pack/test/plugin_functional/test_suites/global_search/global_search_bar.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { - describe('GlobalSearchBar', function () { + describe('TOTO GlobalSearchBar', function () { const { common, navigationalSearch } = getPageObjects(['common', 'navigationalSearch']); const esArchiver = getService('esArchiver'); const browser = getService('browser'); @@ -61,6 +61,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); it('shows a suggestion when searching for a term matching a tag name', async () => { @@ -94,6 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -111,6 +113,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { 'dashboard 1 (tag-2)', 'dashboard 2 (tag-3)', 'dashboard 3 (tag-1 and tag-3)', + 'dashboard 4 (tag-special-chars)', ]); }); @@ -181,6 +184,14 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(results.map((result) => result.label)).to.eql(['My awesome vis (tag-4)']); }); + it('allows to filter by tags containing special characters', async () => { + await navigationalSearch.searchFor('tag:"my%tag"'); + + const results = await navigationalSearch.getDisplayedResults(); + + expect(results.map((result) => result.label)).to.eql(['dashboard 4 (tag-special-chars)']); + }); + it('returns no results when searching for an unknown tag', async () => { await navigationalSearch.searchFor('tag:unknown'); diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts index bd7fa7538703c..30008e635b628 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/create.ts @@ -60,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/create`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts index 7b4298607c666..ddf39ccf90b34 100644 --- a/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts +++ b/x-pack/test/saved_object_tagging/api_integration/tagging_api/apis/update.ts @@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) { await supertest .post(`/api/saved_objects_tagging/tags/tag-1`) .send({ - name: 'Inv%li& t@g n*me', + name: 'a', description: 'some desc', color: 'this is not a valid color', }) @@ -92,7 +92,7 @@ export default function ({ getService }: FtrProviderContext) { valid: false, warnings: [], errors: { - name: 'Tag name can only include a-z, 0-9, _, -,:.', + name: 'Tag name must be at least 2 characters', color: 'Tag color must be a valid hex color', }, }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/create.ts b/x-pack/test/saved_object_tagging/functional/tests/create.ts index b62e9a70b43e8..2f2db856c0657 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/create.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/create.ts @@ -54,7 +54,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, @@ -73,7 +73,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openCreate(); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'The name will fails validation', color: '#FF00CC', }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/edit.ts b/x-pack/test/saved_object_tagging/functional/tests/edit.ts index 1883d3f23dc9d..1de101433179d 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/edit.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/edit.ts @@ -71,7 +71,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', }, { submit: true } ); @@ -88,7 +88,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await tagModal.openEdit('tag-2'); await tagModal.fillForm( { - name: 'invalid&$%name', + name: 'a', description: 'edited description', color: '#FF00CC', }, diff --git a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts index e92ba226f3959..51f4bf8883521 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/visualize_integration.ts @@ -151,7 +151,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('editing', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88639 + describe.skip('editing', () => { beforeEach(async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.waitUntilTableIsLoaded(); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts index f06e8eba0bf68..e3797550984aa 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -20,7 +20,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const retry = getService('retry'); - describe('Search search sessions Management UI', () => { + // FLAKY: https://github.com/elastic/kibana/issues/89069 + describe.skip('Search search sessions Management UI', () => { describe('New search sessions', () => { before(async () => { await PageObjects.common.navigateToApp('dashboard'); diff --git a/yarn.lock b/yarn.lock index cc32349b10860..e7870415b0dda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5137,10 +5137,10 @@ dependencies: "@types/sizzle" "*" -"@types/js-cookie@2.2.5": - version "2.2.5" - resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.5.tgz#38dfaacae8623b37cc0b0d27398e574e3fc28b1e" - integrity sha512-cpmwBRcHJmmZx0OGU7aPVwGWGbs4iKwVYchk9iuMtxNCA2zorwdaTz4GkLgs2WGxiRZRFKnV1k6tRUHX7tBMxg== +"@types/js-cookie@2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" + integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== "@types/js-search@^1.4.0": version "1.4.0" @@ -6374,10 +6374,10 @@ dependencies: tslib "^1.9.3" -"@xobotyi/scrollbar-width@1.9.4": - version "1.9.4" - resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.4.tgz#a7dce20b7465bcad29cd6bbb557695e4ea7863cb" - integrity sha512-o12FCQt/X5n3pgKEWGpt0f/7Eg4mfv3uRwPUrctiOT8ZuxbH3cNLGWfH/8y6KxVJg4L2885ucuXQ6XECZzUiJA== +"@xobotyi/scrollbar-width@1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== "@xtuc/ieee754@^1.2.0": version "1.2.0" @@ -13368,7 +13368,7 @@ fancy-log@^1.3.2: color-support "^1.1.3" time-stamp "^1.0.0" -fast-deep-equal@^3.1.1, fast-deep-equal@~3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3, fast-deep-equal@~3.1.3: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== @@ -23597,24 +23597,30 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-use@^13.27.0: - version "13.27.0" - resolved "https://registry.yarnpkg.com/react-use/-/react-use-13.27.0.tgz#53a619dc9213e2cbe65d6262e8b0e76641ade4aa" - integrity sha512-2lyTyqJWyvnaP/woVtDcFS4B5pUYz0FQWI9pVHk/6TBWom2x3/ziJthkEn/LbCA9Twv39xSQU7Dn0zdIWfsNTQ== +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^15.3.4: + version "15.3.4" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-15.3.4.tgz#f853d310bd71f75b38900a8caa3db93f6dc6e872" + integrity sha512-cHq1dELW6122oi1+xX7lwNyE/ugZs5L902BuO8eFJCfn2api1KeuPVG1M/GJouVARoUf54S2dYFMKo5nQXdTag== dependencies: - "@types/js-cookie" "2.2.5" - "@xobotyi/scrollbar-width" "1.9.4" + "@types/js-cookie" "2.2.6" + "@xobotyi/scrollbar-width" "1.9.5" copy-to-clipboard "^3.2.0" - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" fast-shallow-equal "^1.0.0" js-cookie "^2.2.1" nano-css "^5.2.1" + react-universal-interface "^0.6.2" resize-observer-polyfill "^1.5.1" screenfull "^5.0.0" set-harmonic-interval "^1.0.1" throttle-debounce "^2.1.0" ts-easing "^0.2.0" - tslib "^1.10.0" + tslib "^2.0.0" react-virtualized-auto-sizer@^1.0.2: version "1.0.2" @@ -28645,10 +28651,10 @@ vega-functions@^5.10.0: vega-time "^2.0.4" vega-util "^1.16.0" -vega-functions@~5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.11.0.tgz#a590d016f93c81730bdbc336b377231d7ae48569" - integrity sha512-/p0QIDiA3RaUZ7drxHuClpDQCrIScSHJlY0oo0+GFYGfp+lvb29Ox1T4a+wtqeCp6NRaTWry+EwDxojnshTZIQ== +vega-functions@^5.12.0, vega-functions@~5.12.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/vega-functions/-/vega-functions-5.12.0.tgz#44bf08a7b20673dc8cf51d6781c8ea1399501668" + integrity sha512-3hljmGs+gR7TbO/yYuvAP9P5laKISf1GKk4yRHLNdM61fWgKm8pI3f6LY2Hvq9cHQFTiJ3/5/Bx2p1SX5R4quQ== dependencies: d3-array "^2.7.1" d3-color "^2.0.0" @@ -28656,8 +28662,8 @@ vega-functions@~5.11.0: vega-dataflow "^5.7.3" vega-expression "^4.0.1" vega-scale "^7.1.1" - vega-scenegraph "^4.9.2" - vega-selections "^5.2.0" + vega-scenegraph "^4.9.3" + vega-selections "^5.3.0" vega-statistics "^1.7.9" vega-time "^2.0.4" vega-util "^1.16.0" @@ -28724,16 +28730,16 @@ vega-loader@^4.3.2, vega-loader@^4.3.3, vega-loader@~4.4.0: vega-format "^1.0.4" vega-util "^1.16.0" -vega-parser@~6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.2.tgz#7f25751177e38c3239560a9c427ded8d2ba617bb" - integrity sha512-aGyZrNzPrBruEb/WhemKDuDjQsIkMDGIgnSJci0b+9ZVxjyAzMl7UfGbiYorPiJlnIercjUJbMoFD6fCIf4gqQ== +vega-parser@~6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/vega-parser/-/vega-parser-6.1.3.tgz#df72785e4b086eceb90ee6219a399210933b507b" + integrity sha512-8oiVhhW26GQ4GZBvolId8FVFvhn3s1KGgPlD7Z+4P2wkV+xe5Nqu0TEJ20F/cn3b88fd0Vj48X3BH3dlSeKNFg== dependencies: vega-dataflow "^5.7.3" vega-event-selector "^2.0.6" - vega-functions "^5.10.0" + vega-functions "^5.12.0" vega-scale "^7.1.1" - vega-util "^1.15.2" + vega-util "^1.16.0" vega-projection@^1.4.5, vega-projection@~1.4.5: version "1.4.5" @@ -28772,7 +28778,7 @@ vega-scale@^7.0.3, vega-scale@^7.1.1, vega-scale@~7.1.1: vega-time "^2.0.4" vega-util "^1.15.2" -vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: +vega-scenegraph@^4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.2.tgz#83b1dbc34a9ab5595c74d547d6d95849d74451ed" integrity sha512-epm1CxcB8AucXQlSDeFnmzy0FCj+HV2k9R6ch2lfLRln5lPLEfgJWgFcFhVf5jyheY0FSeHH52Q5zQn1vYI1Ow== @@ -28784,6 +28790,18 @@ vega-scenegraph@^4.9.2, vega-scenegraph@~4.9.2: vega-scale "^7.1.1" vega-util "^1.15.2" +vega-scenegraph@^4.9.3, vega-scenegraph@~4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/vega-scenegraph/-/vega-scenegraph-4.9.3.tgz#c4720550ea7ff5c8d9d0690f47fe2640547cfc6b" + integrity sha512-lBvqLbXqrqRCTGJmSgzZC/tLR/o+TXfakbdhDzNdpgTavTaQ65S/67Gpj5hPpi77DvsfZUIY9lCEeO37aJhy0Q== + dependencies: + d3-path "^2.0.0" + d3-shape "^2.0.0" + vega-canvas "^1.2.5" + vega-loader "^4.3.3" + vega-scale "^7.1.1" + vega-util "^1.15.2" + vega-schema-url-parser@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vega-schema-url-parser/-/vega-schema-url-parser-2.1.0.tgz#847f9cf9f1624f36f8a51abc1adb41ebc6673cb4" @@ -28797,10 +28815,10 @@ vega-selections@^5.1.5: vega-expression "^4.0.0" vega-util "^1.15.2" -vega-selections@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.2.0.tgz#d85968d1bccc175fd92661c91d88151ffd5ade83" - integrity sha512-Xf3nTTJHRGw4tQMbt+0sBI/7WkEIzPG9E4HXkZk5Y9Q2HsGRVLmrAEXHSfpENrBLWTBZk/uvmP9rKDG7cbcTrg== +vega-selections@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/vega-selections/-/vega-selections-5.3.0.tgz#810f2e7b7642fa836cf98b2e5dcc151093b1f6a7" + integrity sha512-vC4NPsuN+IffruFXfH0L3i2A51RgG4PqpLv85TvrEAIYnSkyKDE4bf+wVraR3aPdnLLkc3+tYuMi6le5FmThIA== dependencies: vega-expression "^4.0.1" vega-util "^1.16.0" @@ -28899,10 +28917,10 @@ vega-wordcloud@~4.1.3: vega-statistics "^1.7.9" vega-util "^1.15.2" -vega@^5.18.0: - version "5.18.0" - resolved "https://registry.yarnpkg.com/vega/-/vega-5.18.0.tgz#98645e5d3bd5267d66ea3e701d99dcff63cfff8a" - integrity sha512-ysqouhboWNXSuQNN7W5IGOXsnEJNFVX5duCi0tTwRsFLc61FshpqVh4+4VoXg5pH0ZCxwpqbOwd2ULZWjJTx6g== +vega@^5.19.1: + version "5.19.1" + resolved "https://registry.yarnpkg.com/vega/-/vega-5.19.1.tgz#64c8350740fe1a11d56cc6617ab3a76811fd704c" + integrity sha512-UE6/c9q9kzuz4HULFuU9HscBASoZa+zcXqGKdbQP545Nwmhd078QpcH+wZsq9lYfiTxmFtzLK/a0OH0zhkghvA== dependencies: vega-crossfilter "~4.0.5" vega-dataflow "~5.7.3" @@ -28911,17 +28929,17 @@ vega@^5.18.0: vega-expression "~4.0.1" vega-force "~4.0.7" vega-format "~1.0.4" - vega-functions "~5.11.0" + vega-functions "~5.12.0" vega-geo "~4.3.8" vega-hierarchy "~4.0.9" vega-label "~1.0.0" vega-loader "~4.4.0" - vega-parser "~6.1.2" + vega-parser "~6.1.3" vega-projection "~1.4.5" vega-regression "~1.0.9" vega-runtime "~6.1.3" vega-scale "~7.1.1" - vega-scenegraph "~4.9.2" + vega-scenegraph "~4.9.3" vega-statistics "~1.7.9" vega-time "~2.0.4" vega-transforms "~4.9.3"