diff --git a/dev_docs/tutorials/data/search.mdx b/dev_docs/tutorials/data/search.mdx new file mode 100644 index 0000000000000..69b4d5dab58b5 --- /dev/null +++ b/dev_docs/tutorials/data/search.mdx @@ -0,0 +1,496 @@ +--- +id: kibDevTutorialDataSearchAndSessions +slug: /kibana-dev-docs/tutorials/data/search-and-sessions +title: Kibana data.search Services +summary: Kibana Search Services +date: 2021-02-10 +tags: ['kibana', 'onboarding', 'dev', 'tutorials', 'search', 'sessions', 'search-sessions'] +--- + +## Search service + +### Low level search + +Searching data stored in Elasticsearch can be done in various ways, for example using the Elasticsearch REST API or using an `Elasticsearch Client` for low level access. + +However, the recommended and easist way to search Elasticsearch is by using the low level search service. The service is exposed from the `data` plugin, and by using it, you not only gain access to the data you stored, but also to capabilities, such as Custom Search Strategies, Asynchronous Search, Partial Results, Search Sessions, and more. + +Here is a basic example for using the `data.search` service from a custom plugin: + +```ts +import { CoreStart, Plugin } from 'kibana/public'; +import { DataPublicPluginStart, isCompleteResponse, isErrorResponse } from import { DataPublicPluginStart, isCompleteResponse, isErrorResponse } from '../../src/plugins/data'; + +export interface MyPluginStartDependencies { + data: DataPublicPluginStart; +} + +export class MyPlugin implements Plugin { + public start(core: CoreStart, { data }: MyPluginStartDependencies) { + const query = { + filter: [{ + match_all: {} + }], + }; + const req = { + params: { + index: 'my-index-*', + body: { + query, + aggs: {}, + }, + } + }; + data.search.search(req).subscribe({ + next: (result) => { + if (isCompleteResponse(res)) { + // handle search result + } else if (isErrorResponse(res)) { + // handle error, this means that some results were returned, but the search has failed to complete. + } else { + // handle partial results if you want. + } + }, + error: (e) => { + // handle error thrown, for example a server hangup + }, + }) + } +} +``` + +Note: The `data` plugin contains services to help you generate the `query` and `aggs` portions, as well as managing indices using the `data.indexPatterns` service. + + + The `data.search` service is available on both server and client, with similar APIs. + + +#### Error handling + +The `search` method can throw several types of errors, for example: + + - `EsError` for errors originating in Elasticsearch errors + - `PainlessError` for errors originating from a Painless script + - `AbortError` if the search was aborted via an `AbortController` + - `HttpError` in case of a network error + +To display the errors in the context of an application, use the helper method provided on the `data.search` service. These errors are shown in a toast message, using the `core.notifications` service. + +```ts +data.search.search(req).subscribe({ + next: (result) => {}, + error: (e) => { + data.search.showError(e); + }, +}) +``` + +If you decide to handle errors by yourself, watch for errors coming from `Elasticsearch`. They have an additional `attributes` property that holds the raw error from `Elasticsearch`. + +```ts +data.search.search(req).subscribe({ + next: (result) => {}, + error: (e) => { + if (e instanceof IEsError) { + showErrorReason(e.attributes); + } + }, +}) +``` + +#### Stop a running search + +The search service `search` method supports a second argument called `options`. One of these options provides an `abortSignal` to stop searches from running to completion, if the result is no longer needed. + +```ts +import { AbortError } from '../../src/data/public'; + +const abortController = new AbortController(); +data.search.search(req, { + abortSignal: abortController.signal, +}).subscribe({ + next: (result) => { + // handle result + }, + error: (e) => { + if (e instanceof AbortError) { + // you can ignore this error + return; + } + // handle error, for example a server hangup + }, +}); + +// Abort the search request after a second +setTimeout(() => { + abortController.abort(); +}, 1000); +``` + +#### Search strategies + +By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL. + +For example, to run an EQL query using the `data.search` service, you should to specify the strategy name using the options parameter: + +```ts +const req = getEqlRequest(); +data.search.search(req, { + strategy: EQL_SEARCH_STRATEGY, +}).subscribe({ + next: (result) => { + // handle EQL result + }, +}); +``` + +##### Custom search strategies + +To use a different query syntax, preprocess the request, or process the response before returning it to the client, you can create and register a custom search strategy to encapsulate your custom logic. + +The following example shows how to define, register, and use a search strategy that preprocesses the request before sending it to the default DSL search strategy, and then processes the response before returning. + +```ts +// ./myPlugin/server/myStrategy.ts + +/** + * Your custom search strategy should implement the ISearchStrategy interface, requiring at minimum a `search` function. + */ +export const mySearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy => { + const preprocessRequest = (request: IMyStrategyRequest) => { + // Custom preprocessing + } + + const formatResponse = (response: IMyStrategyResponse) => { + // Custom post-processing + } + + // Get the default search strategy + const es = data.search.getSearchStrategy(ES_SEARCH_STRATEGY); + return { + search: (request, options, deps) => { + return formatResponse(es.search(preprocessRequest(request), options, deps)); + }, + }; +}; +``` + +```ts +// ./myPlugin/server/plugin.ts +import type { + CoreSetup, + CoreStart, + Plugin, +} from 'kibana/server'; + +import { mySearchStrategyProvider } from './my_strategy'; + +/** + * Your plugin will receive the `data` plugin contact in both the setup and start lifecycle hooks. + */ +export interface MyPluginSetupDeps { + data: PluginSetup; +} + +export interface MyPluginStartDeps { + data: PluginStart; +} + +/** + * In your custom server side plugin, register the strategy from the setup contract + */ +export class MyPlugin implements Plugin { + public setup( + core: CoreSetup, + deps: MyPluginSetupDeps + ) { + core.getStartServices().then(([_, depsStart]) => { + const myStrategy = mySearchStrategyProvider(depsStart.data); + deps.data.search.registerSearchStrategy('myCustomStrategy', myStrategy); + }); + } +} +``` + +```ts +// ./myPlugin/public/plugin.ts +const req = getRequest(); +data.search.search(req, { + strategy: 'myCustomStrategy', +}).subscribe({ + next: (result) => { + // handle result + }, +}); +``` + +##### Async search and custom async search strategies + +The open source default search strategy (`ES_SEARCH_STRATEGY`), run searches synchronously, keeping an open connection to Elasticsearch while the query executes. The duration of these queries is restricted by the `elasticsearch.requestTimeout` setting in `kibana.yml`, which is 30 seconds by default. + +This synchronous execution works great in most cases. However, with the introduction of features such as `data tiers` and `runtime fields`, the need to allow slower-running queries, where holding an open connection might be inefficient, has increased. In 7.7, `Elasticsearch` introduced the [async_search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html), allowing a query to run longer without keeping an open connection. Instead, the initial search request returns an ID that identifies the search running in `Elasticsearch`. This ID can then be used to retrieve, cancel, or manage the search result. + +The [async_search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html) is what drives more advanced `Kibana` `search` features, such as `partial results` and `search sessions`. [When available](https://www.elastic.co/subscriptions), the default search strategy of `Kibana` is automatically set to the **async** default search strategy (`ENHANCED_ES_SEARCH_STRATEGY`), empowering Kibana to run longer queries, with an **optional** duration restriction defined by the UI setting `search:timeout`. + +If you are implementing your own async custom search strategy, make sure to implement `cancel` and `extend`, as shown in the following example: + +```ts +// ./myPlugin/server/myEnhancedStrategy.ts +export const myEnhancedSearchStrategyProvider = ( + data: PluginStart +): ISearchStrategy => { + // Get the default search strategy + const ese = data.search.getSearchStrategy(ENHANCED_ES_SEARCH_STRATEGY); + return { + search: (request, options, deps) => { + // search will be called multiple times, + // be sure your response formatting is capable of handling partial results, as well as the final result. + return formatResponse(ese.search(request, options, deps)); + }, + cancel: async (id, options, deps) => { + // call the cancel method of the async strategy you are using or implement your own cancellation function. + await ese.cancel(id, options, deps); + }, + extend: async (id, keepAlive, options, deps) => { + // async search results are not stored indefinitely. By default, they expire after 7 days (or as defined by xpack.data_enhanced.search.sessions.defaultExpiration setting in kibana.yml). + // call the extend method of the async strategy you are using or implement your own extend function. + await ese.extend(id, options, deps); + }, + }; +}; +``` + +### High level search + +The high level search service is a simplified way to create and run search requests, without writing custom DSL queries. + +#### Search source + +```ts +function searchWithSearchSource() { + const indexPattern = data.indexPatterns.getDefault(); + const query = data.query.queryString.getQuery(); + const filters = data.query.filterManager.getFilters(); + const timefilter = data.query.timefilter.timefilter.createFilter(indexPattern); + if (timefilter) { + filters.push(timefilter); + } + + const searchSource = await data.search.searchSource.create(); + + searchSource + .setField('index', indexPattern) + .setField('filter', filters) + .setField('query', query) + .setField('fields', selectedFields.length ? selectedFields.map((f) => f.name) : ['*']) + .setField('aggs', getAggsDsl()); + + searchSource.fetch$().subscribe({ + next: () => {}, + error: () => {}, + }); +} +``` + +### Partial results + +When searching using an `async` strategy (such as async DSL and async EQL), the search service will stream back partial results. + +Although you can ignore the partial results and wait for the final result before rendering, you can also use the partial results to create a more interactive experience for your users. It is highly advised, however, to make sure users are aware that the results they are seeing are partial. + +```ts +// Handling partial results +data.search.search(req).subscribe({ + next: (result) => { + if (isCompleteResponse(res)) { + renderFinalResult(res); + } else if (isPartialResponse(res)) { + renderPartialResult(res); + } + }, +}) + +// Skipping partial results +const finalResult = await data.search.search(req).toPromise(); +``` + +### Search sessions + +A search session is a higher level concept than search. A search session describes a grouping of one or more async search requests with additional context. + +Search sessions are handy when you want to enable a user to run something asynchronously (for example, a dashboard over a long period of time), and then quickly restore the results at a later time. The `Search Service` transparently fetches results from the `.async-search` index, instead of running each request again. + +Internally, any search run within a search session is saved into an object, allowing Kibana to manage their lifecycle. Most saved objects are deleted automatically after a short period of time, but if a user chooses to save the search session, the saved object is persisted, so that results can be restored in a later time. + +Stored search sessions are listed in the *Management* application, under *Kibana > Search Sessions*, making it easy to find, manage, and restore them. + +As a developer, you might encounter these two common, use cases: + + * Running a search inside an existing search session + * Supporting search sessions in your application + +#### Running a search inside an existing search session + +For this example, assume you are implementing a new type of `Embeddable` that will be shown on dashboards. The same principle applies, however, to any search requests that you are running, as long as the application you are running inside is managing an active session. + +Because the Dashboard application is already managing a search session, all you need to do is pass down the `searchSessionId` argument to any `search` call. This applies to both the low and high level search APIs. + +The search information will be added to the saved object for the search session. + +```ts +export class SearchEmbeddable + extends Embeddable { + + private async fetchData() { + // Every embeddable receives an optional `searchSessionId` input parameter. + const { searchSessionId } = this.input; + + // Setup your search source + this.configureSearchSource(); + + try { + // Mark the embeddable as loading + this.updateOutput({ loading: true, error: undefined }); + + // Make the request, wait for the final result + const resp = await searchSource.fetch$({ + sessionId: searchSessionId, + }).toPromise(); + + this.useSearchResult(resp); + + this.updateOutput({ loading: false, error: undefined }); + } catch (error) { + // handle search errors + this.updateOutput({ loading: false, error }); + } + } +} + +``` + +You can also retrieve the active `Search Session ID` from the `Search Service` directly: + +```ts +async function fetchData(data: DataPublicPluginStart) { + try { + return await searchSource.fetch$({ + sessionId: data.search.sessions.getSessionId(), + }).toPromise(); + } catch (e) { + // handle search errors + } +} + +``` + + + Search sessions are initiated by the client. If you are using a route that runs server side searches, you can send the `searchSessionId` to the server, and then pass it down to the server side `data.search` function call. + + +#### Supporting search sessions in your application + +Before implementing the ability to create and restore search sessions in your application, ask yourself the following questions: + +1. **Does your application normally run long operations?** For example, it makes sense for a user to generate a Dashboard or a Canvas report from data stored in cold storage. However, when editing a single visualization, it is best to work with a shorter timeframe of hot or warm data. +2. **Does it make sense for your application to restore a search session?** For example, you might want to restore an interesting configuration of filters of older documents you found in Discover. However, a single Lens or Map visualization might not be as helpful, outside the context of a specific dashboard. +3. **What is a search session in the context of your application?** Although Discover and Dashboard start a new search session every time the time range or filters change, or when the user clicks **Refresh**, you can manage your sessions differently. For example, if your application has tabs, you might group searches from multiple tabs into a single search session. You must be able to clearly define the **state** used to create the search session`. The **state** refers to any setting that might change the queries being set to `Elasticsearch`. + +Once you answer those questions, proceed to implement the following bits of code in your application. + +##### Provide storage configuration + +In your plugin's `start` lifecycle method, call the `enableStorage` method. This method helps the `Session Service` gather the information required to save the search sessions upon a user's request and construct the restore state: + +```ts +export class MyPlugin implements Plugin { + public start(core: CoreStart, { data }: MyPluginStartDependencies) { + const sessionRestorationDataProvider: SearchSessionInfoProvider = { + data, + getDashboard + } + + data.search.session.enableStorage({ + getName: async () => { + // return the name you want to give the saved Search Session + return `MyApp_${Math.random()}`; + }, + getUrlGeneratorData: async () => { + return { + urlGeneratorId: MY_URL_GENERATOR, + initialState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: false }), + restoreState: getUrlGeneratorState({ ...deps, shouldRestoreSearchSession: true }), + }; + }, + }); + } +} +``` + + + The restore state of a search session may be different from the initial state used to create it. For example, where the initial state may contain relative dates, in the restore state, those must be converted to absolute dates. Read more about the [NowProvider](). + + + + Calling `enableStorage` will also enable the `Search Session Indicator` component in the chrome component of your solution. The `Search Session Indicator` is a small button, used by default to engage users and save new search sessions. To implement your own UI, contact the Kibana application services team to decouple this behavior. + + +##### Start a new search session + +Make sure to call `start` when the **state** you previously defined changes. + +```ts + +function onSearchSessionConfigChange() { + this.searchSessionId = data.search.sessions.start(); +} + +``` + +Pass the `searchSessionId` to every `search` call inside your application. If you're using `Embeddables`, pass down the `searchSessionId` as `input`. + +If you can't pass the `searchSessionId` directly, you can retrieve it from the service. + +```ts +const currentSearchSessionId = data.search.sessions.getSessionId(); + +``` + +##### Clear search sessions + +Creating a new search session clears the previous one. You must explicitly `clear` the search session when your application is being destroyed: + +```ts +function onDestroy() { + data.search.session.clear(); +} + +``` + +If you don't call `clear`, you will see a warning in the console while developing. However, when running in production, you will get a fatal error. This is done to avoid leakage of unrelated search requests into an existing search session left open by mistake. + +##### Restore search sessions + +The last step of the integration is restoring an existing search session. The `searchSessionId` parameter and the rest of the restore state are passed into the application via the URL. Non-URL support is planned for future releases. + +If you detect the presense of a `searchSessionId` parameter in the URL, call the `restore` method **instead** of calling `start`. The previous example would now become: + +```ts + +function onSearchSessionConfigChange(searchSessionIdFromUrl?: string) { + if (searchSessionIdFromUrl) { + data.search.sessions.restore(searchSessionIdFromUrl); + } else { + data.search.sessions.start(); + } +} + +``` + +Once you `restore` the session, as long as all `search` requests run with the same `searchSessionId`, the search session should be seamlessly restored. + +##### Customize the user experience + +TBD diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index fa44d3a6ae41b..d6f5fb1baba8e 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -21,9 +21,12 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== -| `xpack.encryptedSavedObjects.encryptionKey` +| `xpack.encryptedSavedObjects` +`.encryptionKey` | A string of 32 or more characters used to encrypt sensitive properties on alerts and actions before they're stored in {es}. Third party credentials — such as the username and password used to connect to an SMTP service — are an example of encrypted properties. + + + {kib} offers a <> to help generate this encryption key. + + + If not set, {kib} will generate a random key on startup, but all alert and action functions will be blocked. Generated keys are not allowed for alerts and actions because when a new key is generated on restart, existing encrypted data becomes inaccessible. For the same reason, alerts and actions in high-availability deployments of {kib} will behave unexpectedly if the key isn't the same on all instances of {kib}. + + Although the key can be specified in clear text in `kibana.yml`, it's recommended to store this key securely in the <>. diff --git a/docs/user/alerting/defining-alerts.asciidoc b/docs/user/alerting/defining-alerts.asciidoc index 5f13c6e1774a8..77a4e5cc41ef2 100644 --- a/docs/user/alerting/defining-alerts.asciidoc +++ b/docs/user/alerting/defining-alerts.asciidoc @@ -59,7 +59,7 @@ image::images/alert-flyout-action-type-selection.png[UI for selecting an action When an alert instance matches a condition, the alert is marked as _Active_ and assigned an action group. The actions in that group are triggered. When the condition is no longer detected, the alert is assigned to the _Recovered_ action group, which triggers any actions assigned to that group. -**Run When** allows you to assign an action to an _action group_. This will trigger the action in accordance with your **Notify every** setting. +**Run When** allows you to assign an action to an action group. This will trigger the action in accordance with your **Notify** setting. Each action must specify a <> instance. If no connectors exist for that action type, click *Add action* to create one. @@ -68,7 +68,20 @@ Each action type exposes different properties. For example an email action allow [role="screenshot"] image::images/alert-flyout-action-details.png[UI for defining an email action] -Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. Available variables differ by alert type, and the list of available variables can be accessed using the "add variable" button. +[float] +==== Action variables +Using the https://mustache.github.io/[Mustache] template syntax `{{variable name}}`, you can pass alert values at the time a condition is detected to an action. You can access the list of available variables using the "add variable" button. Although available variables differ by alert type, all alert types pass the following variables: + +`alertId`:: The ID of the alert. +`alertName`:: The name of the alert. +`spaceId`:: The ID of the space for the alert. +`tags`:: The list of tags applied to the alert. +`date`:: The date the alert scheduled the action, in ISO format. +`alertInstanceId`:: The ID of the alert instance that scheduled the action. +`alertActionGroup`:: The ID of the action group of the alert instance that scheduled the action. +`alertActionSubgroup`:: The action subgroup of the alert instance that scheduled the action. +`alertActionGroupName`:: The name of the action group of the alert instance that scheduled the action. +`kibanaBaseUrl`:: The configured <>. If not configured, this will be empty. [role="screenshot"] image::images/alert-flyout-action-variables.png[Passing alert values to an action] diff --git a/docs/user/alerting/images/alert-types-es-query-example-action-variable.png b/docs/user/alerting/images/alert-types-es-query-example-action-variable.png new file mode 100644 index 0000000000000..7e40499d78fdd Binary files /dev/null and b/docs/user/alerting/images/alert-types-es-query-example-action-variable.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-conditions.png b/docs/user/alerting/images/alert-types-index-threshold-conditions.png index 5d66123ac733e..062b0a426b5d8 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-conditions.png and b/docs/user/alerting/images/alert-types-index-threshold-conditions.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png index 055b643ec3458..a43c4bf1f0d37 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png and b/docs/user/alerting/images/alert-types-index-threshold-example-aggregation.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png index 5be81b45612bc..9f4c2ccbec3c0 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png and b/docs/user/alerting/images/alert-types-index-threshold-example-grouping.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-index.png b/docs/user/alerting/images/alert-types-index-threshold-example-index.png index b13201ce5d38a..b2f1c78f7add8 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-index.png and b/docs/user/alerting/images/alert-types-index-threshold-example-index.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-preview.png b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png index 70e1355004c47..19ad52c45da1c 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-preview.png and b/docs/user/alerting/images/alert-types-index-threshold-example-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png index 7e9432d8c8678..9d9262dd96a1e 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png and b/docs/user/alerting/images/alert-types-index-threshold-example-threshold.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png index 4b1eaa631dc98..e7b13ed6e2cc0 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png and b/docs/user/alerting/images/alert-types-index-threshold-example-timefield.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-example-window.png b/docs/user/alerting/images/alert-types-index-threshold-example-window.png index b4b272d2a241a..9b8e9a47ae91e 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-example-window.png and b/docs/user/alerting/images/alert-types-index-threshold-example-window.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-preview.png b/docs/user/alerting/images/alert-types-index-threshold-preview.png index b3b868dbc41e8..2065cbd117b75 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-preview.png and b/docs/user/alerting/images/alert-types-index-threshold-preview.png differ diff --git a/docs/user/alerting/images/alert-types-index-threshold-select.png b/docs/user/alerting/images/alert-types-index-threshold-select.png index 18c28a703e966..7a68d8815b6d9 100644 Binary files a/docs/user/alerting/images/alert-types-index-threshold-select.png and b/docs/user/alerting/images/alert-types-index-threshold-select.png differ diff --git a/docs/user/alerting/stack-alerts/es-query.asciidoc b/docs/user/alerting/stack-alerts/es-query.asciidoc index 772178c9552da..9f4a882328b9f 100644 --- a/docs/user/alerting/stack-alerts/es-query.asciidoc +++ b/docs/user/alerting/stack-alerts/es-query.asciidoc @@ -28,6 +28,27 @@ condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. Time window:: This clause determines how far back to search for documents, using the *time field* set in the *index* clause. Generally this value should be set to a value higher than the *check every* value in the <>, to avoid gaps in detection. +[float] +==== Action variables + +When the ES query alert condition is met, the following variables are available to use inside each action: + +`context.title`:: A preconstructed title for the alert. Example: `alert term match alert query matched`. +`context.message`:: A preconstructed message for the alert. Example: + +`alert 'term match alert' is active:` + +`- Value: 42` + +`- Conditions Met: count greater than 4 over 5m` + +`- Timestamp: 2020-01-01T00:00:00.000Z` + +`context.group`:: The name of the action group associated with the condition. Example: `query matched`. +`context.date`:: The date, in ISO format, that the alert met the condition. Example: `2020-01-01T00:00:00.000Z`. +`context.value`:: The value of the alert that met the condition. +`context.conditions`:: A description of the condition. Example: `count greater than 4`. +`context.hits`:: The most recent ES documents that matched the query. Using the https://mustache.github.io/[Mustache] template array syntax, you can iterate over these hits to get values from the ES documents into your actions. + +[role="screenshot"] +image::images/alert-types-es-query-example-action-variable.png[Iterate over hits using Mustache template syntax] + [float] ==== Testing your query diff --git a/docs/user/alerting/stack-alerts/index-threshold.asciidoc b/docs/user/alerting/stack-alerts/index-threshold.asciidoc index 424320aea3adc..6b45f69401c4a 100644 --- a/docs/user/alerting/stack-alerts/index-threshold.asciidoc +++ b/docs/user/alerting/stack-alerts/index-threshold.asciidoc @@ -31,6 +31,23 @@ If data is available and all clauses have been defined, a preview chart will ren [role="screenshot"] image::user/alerting/images/alert-types-index-threshold-preview.png[Five clauses define the condition to detect] +[float] +==== Action variables + +When the index threshold alert condition is met, the following variables are available to use inside each action: + +`context.title`:: A preconstructed title for the alert. Example: `alert kibana sites - high egress met threshold`. +`context.message`:: A preconstructed message for the alert. Example: + +`alert 'kibana sites - high egress' is active for group 'threshold met':` + +`- Value: 42` + +`- Conditions Met: count greater than 4 over 5m` + +`- Timestamp: 2020-01-01T00:00:00.000Z` + +`context.group`:: The name of the action group associated with the threshold condition. Example: `threshold met`. +`context.date`:: The date, in ISO format, that the alert met the threshold condition. Example: `2020-01-01T00:00:00.000Z`. +`context.value`:: The value for the alert that met the threshold condition. +`context.conditions`:: A description of the threshold condition. Example: `count greater than 4` + [float] ==== Example diff --git a/package.json b/package.json index 3c628de532019..9cf6f97fcafbf 100644 --- a/package.json +++ b/package.json @@ -74,11 +74,11 @@ "**/cross-fetch/node-fetch": "^2.6.1", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", - "**/graphql-toolkit/lodash": "^4.17.15", + "**/graphql-toolkit/lodash": "^4.17.21", "**/hoist-non-react-statics": "^3.3.2", "**/isomorphic-fetch/node-fetch": "^2.6.1", "**/istanbul-instrumenter-loader/schema-utils": "1.0.0", - "**/load-grunt-config/lodash": "^4.17.20", + "**/load-grunt-config/lodash": "^4.17.21", "**/minimist": "^1.2.5", "**/node-jose/node-forge": "^0.10.0", "**/prismjs": "1.22.0", @@ -233,7 +233,7 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "load-json-file": "^6.2.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "lru-cache": "^4.1.5", "markdown-it": "^10.0.0", "md5": "^2.1.0", @@ -390,7 +390,6 @@ "@storybook/addon-essentials": "^6.0.26", "@storybook/addon-knobs": "^6.0.26", "@storybook/addon-storyshots": "^6.0.26", - "@storybook/addons": "^6.0.16", "@storybook/components": "^6.0.26", "@storybook/core": "^6.0.26", "@storybook/core-events": "^6.0.26", diff --git a/src/dev/ci_setup/setup.sh b/src/dev/ci_setup/setup.sh index c4559029e5607..f9c1e67c0540d 100755 --- a/src/dev/ci_setup/setup.sh +++ b/src/dev/ci_setup/setup.sh @@ -21,6 +21,10 @@ cp "src/dev/ci_setup/.bazelrc-ci" "$HOME/.bazelrc"; echo "# Appended by src/dev/ci_setup/setup.sh" >> "$HOME/.bazelrc" echo "build --remote_header=x-buildbuddy-api-key=$KIBANA_BUILDBUDDY_CI_API_KEY" >> "$HOME/.bazelrc" +if [[ "$BUILD_TS_REFS_CACHE_ENABLE" != "true" ]]; then + export BUILD_TS_REFS_CACHE_ENABLE=false +fi + ### ### install dependencies ### diff --git a/src/dev/typescript/build_ts_refs_cli.ts b/src/dev/typescript/build_ts_refs_cli.ts index fc8911a251773..a073e58623278 100644 --- a/src/dev/typescript/build_ts_refs_cli.ts +++ b/src/dev/typescript/build_ts_refs_cli.ts @@ -23,7 +23,7 @@ export async function runBuildRefsCli() { async ({ log, flags }) => { const outDirs = getOutputsDeep(REF_CONFIG_PATHS); - const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE === 'true' || !!flags.cache; + const cacheEnabled = process.env.BUILD_TS_REFS_CACHE_ENABLE !== 'false' && !!flags.cache; const doCapture = process.env.BUILD_TS_REFS_CACHE_CAPTURE === 'true'; const doClean = !!flags.clean || doCapture; const doInitCache = cacheEnabled && !doClean; @@ -62,6 +62,9 @@ export async function runBuildRefsCli() { description: 'Build TypeScript projects', flags: { boolean: ['clean', 'cache'], + default: { + cache: true, + }, }, log: { defaultLevel: 'debug', diff --git a/src/dev/typescript/ref_output_cache/ref_output_cache.ts b/src/dev/typescript/ref_output_cache/ref_output_cache.ts index 342470ce0c6e3..6f51243e47555 100644 --- a/src/dev/typescript/ref_output_cache/ref_output_cache.ts +++ b/src/dev/typescript/ref_output_cache/ref_output_cache.ts @@ -132,7 +132,7 @@ export class RefOutputCache { this.log.debug(`[${relative}] clearing outDir and replacing with cache`); await del(outDir); await unzip(Path.resolve(tmpDir, cacheName), outDir); - await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), archive.sha); + await Fs.writeFile(Path.resolve(outDir, OUTDIR_MERGE_BASE_FILENAME), this.mergeBase); }); } diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts index 809c4ad1ea1bd..490519187f49e 100644 --- a/x-pack/plugins/case/server/client/cases/mock.ts +++ b/x-pack/plugins/case/server/client/cases/mock.ts @@ -11,6 +11,7 @@ import { ConnectorMappingsAttributes, CaseUserActionsResponse, AssociationType, + CommentResponseAlertsType, } from '../../../common/api'; import { BasicParams } from './types'; @@ -76,6 +77,20 @@ export const commentAlert: CommentResponse = { version: 'WzEsMV0=', }; +export const commentAlertMultipleIds: CommentResponseAlertsType = { + ...commentAlert, + id: 'mock-comment-2', + alertId: ['alert-id-1', 'alert-id-2'], + index: 'alert-index-1', + type: CommentType.alert as const, +}; + +export const commentGeneratedAlert: CommentResponseAlertsType = { + ...commentAlertMultipleIds, + id: 'mock-comment-3', + type: CommentType.generatedAlert as const, +}; + export const defaultPipes = ['informationCreated']; export const basicParams: BasicParams = { description: 'a description', diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts index f1d56e7132bd1..2dd2caf9fe73a 100644 --- a/x-pack/plugins/case/server/client/cases/types.ts +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -72,7 +72,7 @@ export interface TransformFieldsArgs { export interface ExternalServiceComment { comment: string; - commentId: string; + commentId?: string; } export interface MapIncident { diff --git a/x-pack/plugins/case/server/client/cases/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts index 361d0fb561afd..44e7a682aa7ed 100644 --- a/x-pack/plugins/case/server/client/cases/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -17,6 +17,8 @@ import { basicParams, userActions, commentAlert, + commentAlertMultipleIds, + commentGeneratedAlert, } from './mock'; import { @@ -48,7 +50,7 @@ describe('utils', () => { { actionType: 'overwrite', key: 'short_description', - pipes: ['informationCreated'], + pipes: [], value: 'a title', }, { @@ -71,7 +73,7 @@ describe('utils', () => { { actionType: 'overwrite', key: 'short_description', - pipes: ['myTestPipe'], + pipes: [], value: 'a title', }, { @@ -98,7 +100,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title', description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }); }); @@ -122,13 +124,13 @@ describe('utils', () => { }, fields, currentIncident: { - short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'first title', description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, }); expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', + short_description: 'a title', description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-15T08:34:53.450Z by Another User)', }); @@ -168,7 +170,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + short_description: 'a title', description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', }); }); @@ -190,7 +192,7 @@ describe('utils', () => { }); expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by anotherUser)', + short_description: 'a title', description: 'a description (updated at 2020-03-15T08:34:53.450Z by anotherUser)', }); }); @@ -448,8 +450,7 @@ describe('utils', () => { labels: ['defacement'], issueType: null, parent: null, - short_description: - 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)', + short_description: 'Super Bad Security Issue', description: 'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)', externalId: null, @@ -504,7 +505,7 @@ describe('utils', () => { expect(res.comments).toEqual([]); }); - it('it creates comments of type alert correctly', async () => { + it('it adds the total alert comments correctly', async () => { const res = await createIncident({ actionsClient: actionsMock, theCase: { @@ -512,7 +513,9 @@ describe('utils', () => { comments: [ { ...commentObj, id: 'comment-user-1' }, { ...commentAlert, id: 'comment-alert-1' }, - { ...commentAlert, id: 'comment-alert-2' }, + { + ...commentAlertMultipleIds, + }, ], }, // Remove second push @@ -536,14 +539,36 @@ describe('utils', () => { commentId: 'comment-user-1', }, { - comment: - 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', - commentId: 'comment-alert-1', + comment: 'Elastic Security Alerts attached to the case: 3', }, + ]); + }); + + it('it removes alerts correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [ + { ...commentObj, id: 'comment-user-1' }, + commentAlertMultipleIds, + commentGeneratedAlert, + ], + }, + userActions, + connector, + mappings, + alerts: [], + }); + + expect(res.comments).toEqual([ { comment: - 'Alert with ids alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', - commentId: 'comment-alert-2', + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + { + comment: 'Elastic Security Alerts attached to the case: 4', }, ]); }); @@ -578,8 +603,7 @@ describe('utils', () => { description: 'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)', externalId: 'external-id', - short_description: - 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)', + short_description: 'Super Bad Security Issue', }, comments: [], }); diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index fda4142bf77c7..a5013d9b93982 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -40,6 +40,15 @@ import { } from './types'; import { getAlertIds } from '../../routes/api/utils'; +interface CreateIncidentArgs { + actionsClient: ActionsClient; + theCase: CaseResponse; + userActions: CaseUserActionsResponse; + connector: ActionConnector; + mappings: ConnectorMappingsAttributes[]; + alerts: CaseClientGetAlertsResponse; +} + export const getLatestPushInfo = ( connectorId: string, userActions: CaseUserActionsResponse @@ -75,14 +84,13 @@ const getCommentContent = (comment: CommentResponse): string => { return ''; }; -interface CreateIncidentArgs { - actionsClient: ActionsClient; - theCase: CaseResponse; - userActions: CaseUserActionsResponse; - connector: ActionConnector; - mappings: ConnectorMappingsAttributes[]; - alerts: CaseClientGetAlertsResponse; -} +const countAlerts = (comments: CaseResponse['comments']): number => + comments?.reduce((total, comment) => { + if (comment.type === CommentType.alert || comment.type === CommentType.generatedAlert) { + return total + (Array.isArray(comment.alertId) ? comment.alertId.length : 1); + } + return total; + }, 0) ?? 0; export const createIncident = async ({ actionsClient, @@ -152,22 +160,34 @@ export const createIncident = async ({ userActions .slice(latestPushInfo?.index ?? 0) .filter( - (action, index) => - Array.isArray(action.action_field) && action.action_field[0] === 'comment' + (action) => Array.isArray(action.action_field) && action.action_field[0] === 'comment' ) .map((action) => action.comment_id) ); - const commentsToBeUpdated = caseComments?.filter((comment) => - commentsIdsToBeUpdated.has(comment.id) + + const commentsToBeUpdated = caseComments?.filter( + (comment) => + // We push only user's comments + comment.type === CommentType.user && commentsIdsToBeUpdated.has(comment.id) ); + const totalAlerts = countAlerts(caseComments); + let comments: ExternalServiceComment[] = []; + if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) { const commentsMapping = mappings.find((m) => m.source === 'comments'); if (commentsMapping?.action_type !== 'nothing') { comments = transformComments(commentsToBeUpdated, ['informationAdded']); } } + + if (totalAlerts > 0) { + comments.push({ + comment: `Elastic Security Alerts attached to the case: ${totalAlerts}`, + }); + } + return { incident, comments }; }; @@ -247,7 +267,13 @@ export const prepareFieldsForTransformation = ({ key: mapping.target, value: params[mapping.source] ?? '', actionType: mapping.action_type, - pipes: mapping.action_type === 'append' ? [...defaultPipes, 'append'] : defaultPipes, + pipes: + // Do not transform titles + mapping.source !== 'title' + ? mapping.action_type === 'append' + ? [...defaultPipes, 'append'] + : defaultPipes + : [], }, ] : acc, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index bf398d1ffcf40..c8501130493ba 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -170,7 +170,7 @@ describe('Push case', () => { parent: null, priority: 'High', labels: ['LOLBins'], - summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)', + summary: 'Another bad one', description: 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)', externalId: null, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx similarity index 73% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx index e5a76bc586b80..641628c32659c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item.tsx @@ -9,26 +9,29 @@ import React, { useMemo } from 'react'; import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui'; -import { BoostIcon } from '../../../boost_icon'; -import { BOOST_TYPE_TO_DISPLAY_MAP } from '../../../constants'; -import { Boost } from '../../../types'; -import { ValueBadge } from '../../value_badge'; +import { BoostIcon } from '../boost_icon'; +import { BOOST_TYPE_TO_DISPLAY_MAP } from '../constants'; +import { Boost } from '../types'; +import { ValueBadge } from '../value_badge'; +import { BoostItemContent } from './boost_item_content'; import { getBoostSummary } from './get_boost_summary'; interface Props { boost: Boost; id: string; + index: number; + name: string; } -export const BoostItem: React.FC = ({ id, boost }) => { +export const BoostItem: React.FC = ({ id, boost, index, name }) => { const summary = useMemo(() => getBoostSummary(boost), [boost]); return ( @@ -48,6 +51,8 @@ export const BoostItem: React.FC = ({ id, boost }) => { } paddingSize="s" - /> + > + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.tsx new file mode 100644 index 0000000000000..3296155fdce5d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton, EuiRange } from '@elastic/eui'; + +import { BoostType } from '../../types'; + +import { BoostItemContent } from './boost_item_content'; +import { FunctionalBoostForm } from './functional_boost_form'; +import { ProximityBoostForm } from './proximity_boost_form'; +import { ValueBoostForm } from './value_boost_form'; + +describe('BoostItemContent', () => { + const actions = { + updateBoostFactor: jest.fn(), + deleteBoost: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders a value boost form if the provided boost is "value" boost', () => { + const boost = { + factor: 2, + type: 'value' as BoostType, + }; + + const wrapper = shallow(); + + expect(wrapper.find(ValueBoostForm).exists()).toBe(true); + expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false); + expect(wrapper.find(ProximityBoostForm).exists()).toBe(false); + }); + + it('renders a functional boost form if the provided boost is "functional" boost', () => { + const boost = { + factor: 10, + type: 'functional' as BoostType, + }; + + const wrapper = shallow(); + + expect(wrapper.find(ValueBoostForm).exists()).toBe(false); + expect(wrapper.find(FunctionalBoostForm).exists()).toBe(true); + expect(wrapper.find(ProximityBoostForm).exists()).toBe(false); + }); + + it('renders a proximity boost form if the provided boost is "proximity" boost', () => { + const boost = { + factor: 8, + type: 'proximity' as BoostType, + }; + + const wrapper = shallow(); + + expect(wrapper.find(ValueBoostForm).exists()).toBe(false); + expect(wrapper.find(FunctionalBoostForm).exists()).toBe(false); + expect(wrapper.find(ProximityBoostForm).exists()).toBe(true); + }); + + it("renders an impact slider that can be used to update the boost's 'factor'", () => { + const boost = { + factor: 8, + type: 'proximity' as BoostType, + }; + + const wrapper = shallow(); + const impactSlider = wrapper.find(EuiRange); + expect(impactSlider.prop('value')).toBe(8); + + impactSlider.simulate('change', { target: { value: '2' } }); + + expect(actions.updateBoostFactor).toHaveBeenCalledWith('foo', 3, 2); + }); + + it("will delete the current boost if the 'Delete Boost' button is clicked", () => { + const boost = { + factor: 8, + type: 'proximity' as BoostType, + }; + + const wrapper = shallow(); + wrapper.find(EuiButton).simulate('click'); + + expect(actions.deleteBoost).toHaveBeenCalledWith('foo', 3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx new file mode 100644 index 0000000000000..7a19564543c81 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/boost_item_content.tsx @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiButton, EuiFormRow, EuiPanel, EuiRange, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../..'; +import { Boost, BoostType } from '../../types'; + +import { FunctionalBoostForm } from './functional_boost_form'; +import { ProximityBoostForm } from './proximity_boost_form'; +import { ValueBoostForm } from './value_boost_form'; + +interface Props { + boost: Boost; + index: number; + name: string; +} + +export const BoostItemContent: React.FC = ({ boost, index, name }) => { + const { deleteBoost, updateBoostFactor } = useActions(RelevanceTuningLogic); + const { type } = boost; + + const getBoostForm = () => { + switch (type) { + case BoostType.Value: + return ; + case BoostType.Functional: + return ; + case BoostType.Proximity: + return ; + } + }; + + return ( + + {getBoostForm()} + + + + updateBoostFactor( + name, + index, + parseFloat((e as React.ChangeEvent).target.value) + ) + } + showInput + compressed + fullWidth + /> + + deleteBoost(name, index)}> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.deleteBoostButtonLabel', + { + defaultMessage: 'Delete Boost', + } + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx new file mode 100644 index 0000000000000..11a224a71d7f8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSelect } from '@elastic/eui'; + +import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from '../../types'; + +import { FunctionalBoostForm } from './functional_boost_form'; + +describe('FunctionalBoostForm', () => { + const boost: Boost = { + factor: 2, + type: 'functional' as BoostType, + function: 'logarithmic' as FunctionalBoostFunction, + operation: 'multiply' as BoostOperation, + }; + + const actions = { + updateBoostSelectOption: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + const functionSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(0); + const operationSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect).at(1); + + it('renders select boxes with values from the provided boost selected', () => { + const wrapper = shallow(); + expect(functionSelect(wrapper).prop('value')).toEqual('logarithmic'); + expect(operationSelect(wrapper).prop('value')).toEqual('multiply'); + }); + + it('will update state when a user makes a selection', () => { + const wrapper = shallow(); + + functionSelect(wrapper).simulate('change', { + target: { + value: 'exponential', + }, + }); + expect(actions.updateBoostSelectOption).toHaveBeenCalledWith( + 'foo', + 3, + 'function', + 'exponential' + ); + + operationSelect(wrapper).simulate('change', { + target: { + value: 'add', + }, + }); + expect(actions.updateBoostSelectOption).toHaveBeenCalledWith('foo', 3, 'operation', 'add'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.tsx new file mode 100644 index 0000000000000..d677fe5cbc069 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/functional_boost_form.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../..'; +import { + BOOST_OPERATION_DISPLAY_MAP, + FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP, +} from '../../constants'; +import { + Boost, + BoostFunction, + BoostOperation, + BoostType, + FunctionalBoostFunction, +} from '../../types'; + +interface Props { + boost: Boost; + index: number; + name: string; +} + +const functionOptions = Object.values(FunctionalBoostFunction).map((boostFunction) => ({ + value: boostFunction, + text: FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP[boostFunction as FunctionalBoostFunction], +})); + +const operationOptions = Object.values(BoostOperation).map((boostOperation) => ({ + value: boostOperation, + text: BOOST_OPERATION_DISPLAY_MAP[boostOperation as BoostOperation], +})); + +export const FunctionalBoostForm: React.FC = ({ boost, index, name }) => { + const { updateBoostSelectOption } = useActions(RelevanceTuningLogic); + return ( + <> + + + updateBoostSelectOption(name, index, 'function', e.target.value as BoostFunction) + } + fullWidth + /> + + + + updateBoostSelectOption(name, index, 'operation', e.target.value as BoostOperation) + } + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/index.ts new file mode 100644 index 0000000000000..1a13c486ca523 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { BoostItemContent } from './boost_item_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.tsx new file mode 100644 index 0000000000000..6abbcc3d98862 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiFieldText, EuiSelect } from '@elastic/eui'; + +import { Boost, BoostType, ProximityBoostFunction } from '../../types'; + +import { ProximityBoostForm } from './proximity_boost_form'; + +describe('ProximityBoostForm', () => { + const boost: Boost = { + factor: 2, + type: 'proximity' as BoostType, + function: 'linear' as ProximityBoostFunction, + center: '2', + }; + + const actions = { + updateBoostSelectOption: jest.fn(), + updateBoostCenter: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + const functionSelect = (wrapper: ShallowWrapper) => wrapper.find(EuiSelect); + const centerInput = (wrapper: ShallowWrapper) => wrapper.find(EuiFieldText); + + it('renders input with values from the provided boost', () => { + const wrapper = shallow(); + expect(functionSelect(wrapper).prop('value')).toEqual('linear'); + expect(centerInput(wrapper).prop('defaultValue')).toEqual('2'); + }); + + describe('various boost values', () => { + const renderWithBoostValues = (boostValues: { + center?: Boost['center']; + function?: Boost['function']; + }) => { + return shallow( + + ); + }; + + it('will set the center value as a string if the value is a number', () => { + const wrapper = renderWithBoostValues({ center: 0 }); + expect(centerInput(wrapper).prop('defaultValue')).toEqual('0'); + }); + + it('will set the center value as an empty string if the value is undefined', () => { + const wrapper = renderWithBoostValues({ center: undefined }); + expect(centerInput(wrapper).prop('defaultValue')).toEqual(''); + }); + + it('will set the function to Guaussian if it is not already set', () => { + const wrapper = renderWithBoostValues({ function: undefined }); + expect(functionSelect(wrapper).prop('value')).toEqual('gaussian'); + }); + }); + + it('will update state when a user enters input', () => { + const wrapper = shallow(); + + functionSelect(wrapper).simulate('change', { + target: { + value: 'exponential', + }, + }); + expect(actions.updateBoostSelectOption).toHaveBeenCalledWith( + 'foo', + 3, + 'function', + 'exponential' + ); + + centerInput(wrapper).simulate('change', { + target: { + value: '5', + }, + }); + expect(actions.updateBoostCenter).toHaveBeenCalledWith('foo', 3, '5'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx new file mode 100644 index 0000000000000..f01f060bfcee6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/proximity_boost_form.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFieldText, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../..'; +import { PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP } from '../../constants'; +import { Boost, BoostType, ProximityBoostFunction } from '../../types'; + +interface Props { + boost: Boost; + index: number; + name: string; +} + +export const ProximityBoostForm: React.FC = ({ boost, index, name }) => { + const { updateBoostSelectOption, updateBoostCenter } = useActions(RelevanceTuningLogic); + + const currentBoostCenter = boost.center !== undefined ? boost.center.toString() : ''; + const currentBoostFunction = boost.function || ProximityBoostFunction.Gaussian; + + const functionOptions = Object.values(ProximityBoostFunction).map((boostFunction) => ({ + value: boostFunction, + text: PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP[boostFunction as ProximityBoostFunction], + })); + + return ( + <> + + + updateBoostSelectOption( + name, + index, + 'function', + e.target.value as ProximityBoostFunction + ) + } + fullWidth + /> + + + updateBoostCenter(name, index, e.target.value)} + fullWidth + /> + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx new file mode 100644 index 0000000000000..447ca8e178349 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiButton, EuiButtonIcon, EuiFieldText } from '@elastic/eui'; + +import { Boost, BoostType } from '../../types'; + +import { ValueBoostForm } from './value_boost_form'; + +describe('ValueBoostForm', () => { + const boost: Boost = { + factor: 2, + type: 'value' as BoostType, + value: ['bar', '', 'baz'], + }; + + const actions = { + removeBoostValue: jest.fn(), + updateBoostValue: jest.fn(), + addBoostValue: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + const valueInput = (wrapper: ShallowWrapper, index: number) => + wrapper.find(EuiFieldText).at(index); + const removeButton = (wrapper: ShallowWrapper, index: number) => + wrapper.find(EuiButtonIcon).at(index); + const addButton = (wrapper: ShallowWrapper) => wrapper.find(EuiButton); + + it('renders a text input for each value from the boost', () => { + const wrapper = shallow(); + expect(valueInput(wrapper, 0).prop('value')).toEqual('bar'); + expect(valueInput(wrapper, 1).prop('value')).toEqual(''); + expect(valueInput(wrapper, 2).prop('value')).toEqual('baz'); + }); + + it('renders a single empty text box if the boost has no value', () => { + const wrapper = shallow( + + ); + expect(valueInput(wrapper, 0).prop('value')).toEqual(''); + }); + + it('updates the corresponding value in state whenever a user changes the value in a text input', () => { + const wrapper = shallow(); + + valueInput(wrapper, 2).simulate('change', { target: { value: 'new value' } }); + + expect(actions.updateBoostValue).toHaveBeenCalledWith('foo', 3, 2, 'new value'); + }); + + it('deletes a boost value when the Remove Value button is clicked', () => { + const wrapper = shallow(); + + removeButton(wrapper, 2).simulate('click'); + + expect(actions.removeBoostValue).toHaveBeenCalledWith('foo', 3, 2); + }); + + it('adds a new boost value when the Add Value is button clicked', () => { + const wrapper = shallow(); + + addButton(wrapper).simulate('click'); + + expect(actions.addBoostValue).toHaveBeenCalledWith('foo', 3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx new file mode 100644 index 0000000000000..15d19a9741d0a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boost_item_content/value_boost_form.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { + EuiButton, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../..'; +import { Boost } from '../../types'; + +interface Props { + boost: Boost; + index: number; + name: string; +} + +export const ValueBoostForm: React.FC = ({ boost, index, name }) => { + const { updateBoostValue, removeBoostValue, addBoostValue } = useActions(RelevanceTuningLogic); + const values = boost.value || ['']; + + return ( + <> + {values.map((value, valueIndex) => ( + + + updateBoostValue(name, index, valueIndex, e.target.value)} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.valueNameAriaLabel', + { + defaultMessage: 'Value name', + } + )} + autoFocus + /> + + + removeBoostValue(name, index, valueIndex)} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.removeValueAriaLabel', + { + defaultMessage: 'Remove value', + } + )} + /> + + + ))} + + addBoostValue(name, index)}> + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.value.addValueButtonLabel', + { + defaultMessage: 'Add Value', + } + )} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss similarity index 94% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss index 53b3c233301b0..0e9b2b1035b36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.scss @@ -3,7 +3,7 @@ min-width: $euiSizeXXL * 4; } - &__itemContent { + &__itemButton { width: 100%; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx similarity index 65% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx index b313e16c0bda1..75c22d2ae9473 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { setMockActions } from '../../../../../../__mocks__/kea.mock'; +import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; @@ -13,8 +13,11 @@ import { shallow } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; -import { SchemaTypes } from '../../../../../../shared/types'; +import { SchemaTypes } from '../../../../shared/types'; +import { BoostType } from '../types'; + +import { BoostItem } from './boost_item'; import { Boosts } from './boosts'; describe('Boosts', () => { @@ -68,4 +71,33 @@ describe('Boosts', () => { expect(actions.addBoost).toHaveBeenCalledWith('foo', 'functional'); }); + + it('will render a list of boosts', () => { + const boost1 = { + factor: 2, + type: 'value' as BoostType, + }; + const boost2 = { + factor: 10, + type: 'functional' as BoostType, + }; + const boost3 = { + factor: 8, + type: 'proximity' as BoostType, + }; + + const wrapper = shallow( + + ); + + const boostItems = wrapper.find(BoostItem); + expect(boostItems.at(0).prop('boost')).toEqual(boost1); + expect(boostItems.at(1).prop('boost')).toEqual(boost2); + expect(boostItems.at(2).prop('boost')).toEqual(boost3); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx similarity index 86% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx index 1ad27346d2630..d6d43ea7beab0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/boosts.tsx @@ -13,13 +13,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@ import { i18n } from '@kbn/i18n'; -import { TEXT } from '../../../../../../shared/constants/field_types'; -import { SchemaTypes } from '../../../../../../shared/types'; +import { TEXT } from '../../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../../shared/types'; -import { BoostIcon } from '../../../boost_icon'; -import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../../../constants'; -import { RelevanceTuningLogic } from '../../../relevance_tuning_logic'; -import { Boost, BoostType } from '../../../types'; +import { BoostIcon } from '../boost_icon'; +import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../constants'; +import { RelevanceTuningLogic } from '../relevance_tuning_logic'; +import { Boost, BoostType } from '../types'; import { BoostItem } from './boost_item'; @@ -111,7 +111,13 @@ export const Boosts: React.FC = ({ name, type, boosts = [] }) => { {boosts.map((boost, index) => ( - + ))} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts similarity index 80% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts index f6852569213a6..4d78fe8f06739 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Boost, BoostFunction, BoostType, BoostOperation } from '../../../types'; +import { Boost, BoostFunction, BoostType, BoostOperation, FunctionalBoostFunction } from '../types'; import { getBoostSummary } from './get_boost_summary'; @@ -29,6 +29,15 @@ describe('getBoostSummary', () => { }) ).toEqual(''); }); + + it('filters out empty values', () => { + expect( + getBoostSummary({ + ...boost, + value: [' ', '', 'foo', '', 'bar'], + }) + ).toEqual('foo,bar'); + }); }); describe('when the boost type is "proximity"', () => { @@ -55,18 +64,20 @@ describe('getBoostSummary', () => { describe('when the boost type is "functional"', () => { const boost: Boost = { type: BoostType.Functional, - function: BoostFunction.Gaussian, + function: FunctionalBoostFunction.Logarithmic, operation: BoostOperation.Add, factor: 5, }; it('creates a summary that is name of the function and operation', () => { - expect(getBoostSummary(boost)).toEqual('gaussian add'); + expect(getBoostSummary(boost)).toEqual('logarithmic add'); }); it('prints empty if function or operation is missing', () => { expect(getBoostSummary({ ...boost, function: undefined })).toEqual(BoostOperation.Add); - expect(getBoostSummary({ ...boost, operation: undefined })).toEqual(BoostFunction.Gaussian); + expect(getBoostSummary({ ...boost, operation: undefined })).toEqual( + FunctionalBoostFunction.Logarithmic + ); expect(getBoostSummary({ ...boost, function: undefined, operation: undefined })).toEqual(''); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts similarity index 80% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts index f3922ebb0fffe..71b1a6136cf65 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/get_boost_summary.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { Boost, BoostType } from '../../../types'; +import { Boost, BoostType } from '../types'; export const getBoostSummary = (boost: Boost): string => { if (boost.type === BoostType.Value) { - return !boost.value ? '' : boost.value.join(','); + return !boost.value ? '' : boost.value.filter((v) => v.trim() !== '').join(','); } else if (boost.type === BoostType.Proximity) { return boost.function || ''; } else { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boosts/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 9fdbb8e979b31..8131a6a3a57c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -7,7 +7,12 @@ import { i18n } from '@kbn/i18n'; -import { BoostType } from './types'; +import { + BoostOperation, + BoostType, + FunctionalBoostFunction, + ProximityBoostFunction, +} from './types'; export const FIELD_FILTER_CUTOFF = 10; @@ -59,6 +64,7 @@ export const VALUE_DISPLAY = i18n.translate( defaultMessage: 'Value', } ); + export const BOOST_TYPE_TO_DISPLAY_MAP = { [BoostType.Proximity]: PROXIMITY_DISPLAY, [BoostType.Functional]: FUNCTIONAL_DISPLAY, @@ -70,3 +76,62 @@ export const BOOST_TYPE_TO_ICON_MAP = { [BoostType.Functional]: 'tokenFunction', [BoostType.Proximity]: 'tokenGeo', }; + +export const ADD_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addOperationDropDownOptionLabel', + { + defaultMessage: 'Add', + } +); + +export const MULTIPLY_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.multiplyOperationDropDownOptionLabel', + { + defaultMessage: 'Multiply', + } +); + +export const BOOST_OPERATION_DISPLAY_MAP = { + [BoostOperation.Add]: ADD_DISPLAY, + [BoostOperation.Multiply]: MULTIPLY_DISPLAY, +}; + +export const LOGARITHMIC_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.logarithmicBoostFunctionDropDownOptionLabel', + { + defaultMessage: 'Logarithmic', + } +); + +export const GAUSSIAN_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.gaussianFunctionDropDownOptionLabel', + { + defaultMessage: 'Gaussian', + } +); + +export const EXPONENTIAL_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.exponentialFunctionDropDownOptionLabel', + { + defaultMessage: 'Exponential', + } +); + +export const LINEAR_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.linearFunctionDropDownOptionLabel', + { + defaultMessage: 'Linear', + } +); + +export const PROXIMITY_BOOST_FUNCTION_DISPLAY_MAP = { + [ProximityBoostFunction.Gaussian]: GAUSSIAN_DISPLAY, + [ProximityBoostFunction.Exponential]: EXPONENTIAL_DISPLAY, + [ProximityBoostFunction.Linear]: LINEAR_DISPLAY, +}; + +export const FUNCTIONAL_BOOST_FUNCTION_DISPLAY_MAP = { + [FunctionalBoostFunction.Logarithmic]: LOGARITHMIC_DISPLAY, + [FunctionalBoostFunction.Exponential]: EXPONENTIAL_DISPLAY, + [FunctionalBoostFunction.Linear]: LINEAR_DISPLAY, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss index 749fca6f79811..9795564da04d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss @@ -17,4 +17,10 @@ } } } + + .relevanceTuningAccordionItem { + border: none; + border-top: $euiBorderThin; + border-radius: 0; + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx index 6043e7ae65b26..674bb91929a76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx @@ -13,9 +13,9 @@ import { SchemaTypes } from '../../../../shared/types'; import { BoostIcon } from '../boost_icon'; import { Boost, BoostType, SearchField } from '../types'; +import { ValueBadge } from '../value_badge'; import { RelevanceTuningItem } from './relevance_tuning_item'; -import { ValueBadge } from './value_badge'; describe('RelevanceTuningItem', () => { const props = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx index 38cec4825cfe7..f7f4c64622fa6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx @@ -13,8 +13,7 @@ import { SchemaTypes } from '../../../../shared/types'; import { BoostIcon } from '../boost_icon'; import { Boost, SearchField } from '../types'; - -import { ValueBadge } from './value_badge'; +import { ValueBadge } from '../value_badge'; interface Props { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss deleted file mode 100644 index 63718a95551fa..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss +++ /dev/null @@ -1,6 +0,0 @@ -.relevanceTuningForm { - &__itemContent { - border: none; - border-top: $euiBorderThin; - } -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx index 29ab559485d77..e780a4de07252 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx @@ -11,14 +11,12 @@ import { EuiPanel } from '@elastic/eui'; import { SchemaTypes } from '../../../../../shared/types'; +import { Boosts } from '../../boosts'; import { Boost, SearchField } from '../../types'; -import { Boosts } from './boosts'; import { TextSearchToggle } from './text_search_toggle'; import { WeightSlider } from './weight_slider'; -import './relevance_tuning_item_content.scss'; - interface Props { name: string; type: SchemaTypes; @@ -29,7 +27,7 @@ interface Props { export const RelevanceTuningItemContent: React.FC = ({ name, type, boosts, field }) => { return ( <> - + {field && } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index a7ee6f9755fc4..8ce07dc699cbb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -9,7 +9,7 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../ import { nextTick } from '@kbn/test/jest'; -import { Boost, BoostFunction, BoostOperation, BoostType } from './types'; +import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from './types'; import { RelevanceTuningLogic } from './'; @@ -1053,14 +1053,14 @@ describe('RelevanceTuningLogic', () => { 'foo', 1, 'function', - BoostFunction.Exponential + FunctionalBoostFunction.Exponential ); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, type: BoostType.Functional, - function: BoostFunction.Exponential, + function: FunctionalBoostFunction.Exponential, }) ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index 95bd33aac5b9f..16da5868da681 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -11,15 +11,23 @@ export enum BoostType { Proximity = 'proximity', } -export enum BoostFunction { +export enum FunctionalBoostFunction { + Logarithmic = 'logarithmic', + Exponential = 'exponential', + Linear = 'linear', +} + +export enum ProximityBoostFunction { Gaussian = 'gaussian', Exponential = 'exponential', Linear = 'linear', } +export type BoostFunction = FunctionalBoostFunction | ProximityBoostFunction; + export enum BoostOperation { Add = 'add', - Multiple = 'multiply', + Multiply = 'multiply', } export interface BaseBoost { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.scss diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/value_badge.tsx diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx index 59b64de369745..1d75e873f9b18 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -187,272 +187,275 @@ export function LayerPanel( ]); return ( -
- - - - - - - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter((columnId) => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach((columnId) => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, + <> +
+ + + + - )} - - - - - {groups.map((group, groupIndex) => { - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - {group.groupLabel}} - labelType="legend" - key={group.groupId} - isInvalid={isMissing} - error={ - isMissing ? ( -
- {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { - defaultMessage: 'Required dimension', - })} -
- ) : ( - [] - ) - } - > - <> - - {group.accessors.map((accessorConfig, accessorIndex) => { - const { columnId } = accessorConfig; - return ( - -
- { - setActiveDimension({ - isNew: false, - activeGroup: group, - activeId: id, - }); - }} - onRemoveClick={(id: string) => { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: id, - prevState: layerDatasourceState, - }), - activeVisualization.removeDimension({ - layerId, - columnId: id, - prevState: props.visualizationState, - }) - ); - removeButtonRef(id); - }} - > - - -
-
- ); - })} -
- {group.supportsMoreColumns ? ( - { - setActiveDimension({ - activeGroup: group, - activeId: id, - isNew: true, - }); - }} - onDrop={onDrop} - /> - ) : null} - -
- ); - })} - { - if (layerDatasource.updateStateOnCloseDimension) { - const newState = layerDatasource.updateStateOnCloseDimension({ - state: layerDatasourceState, - layerId, - columnId: activeId!, - }); - if (newState) { - props.updateDatasource(datasourceId, newState); - } - } - setActiveDimension(initialActiveDimensionState); - }} - panel={ - <> - {activeGroup && activeId && ( + {layerDatasource && ( + { - if (shouldReplaceDimension || shouldRemoveDimension) { - props.updateAll( - datasourceId, - newState, - shouldRemoveDimension - ? activeVisualization.removeDimension({ - layerId, - columnId: activeId, - prevState: props.visualizationState, - }) - : activeVisualization.setDimension({ - layerId, - groupId: activeGroup.groupId, - columnId: activeId, - prevState: props.visualizationState, - }) - ); - } else { - props.updateDatasource(datasourceId, newState); - } - setActiveDimension({ - ...activeDimension, - isNew: false, + layerId, + state: layerDatasourceState, + activeData: props.framePublicAPI.activeData, + setState: (updater: unknown) => { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter((columnId) => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach((columnId) => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); }); + + props.updateAll(datasourceId, newState, nextVisState); }, }} /> - )} - {activeGroup && - activeId && - !activeDimension.isNew && - activeVisualization.renderDimensionEditor && - activeGroup?.enableDimensionEditor && ( -
- + )} + + + + + {groups.map((group, groupIndex) => { + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + {group.groupLabel}
} + labelType="legend" + key={group.groupId} + isInvalid={isMissing} + error={ + isMissing ? ( +
+ {i18n.translate('xpack.lens.editorFrame.requiredDimensionWarningLabel', { + defaultMessage: 'Required dimension', + })} +
+ ) : ( + [] + ) + } + > + <> + + {group.accessors.map((accessorConfig, accessorIndex) => { + const { columnId } = accessorConfig; + + return ( + +
+ { + setActiveDimension({ + isNew: false, + activeGroup: group, + activeId: id, + }); + }} + onRemoveClick={(id: string) => { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: id, + prevState: layerDatasourceState, + }), + activeVisualization.removeDimension({ + layerId, + columnId: id, + prevState: props.visualizationState, + }) + ); + removeButtonRef(id); + }} + > + + +
+
+ ); + })} +
+ {group.supportsMoreColumns ? ( + { + setActiveDimension({ + activeGroup: group, + activeId: id, + isNew: true, + }); }} + onDrop={onDrop} /> - - )} - - } - /> + ) : null} + + + ); + })} - + - - - - - -
-
+ + + + + +
+
+ + { + if (layerDatasource.updateStateOnCloseDimension) { + const newState = layerDatasource.updateStateOnCloseDimension({ + state: layerDatasourceState, + layerId, + columnId: activeId!, + }); + if (newState) { + props.updateDatasource(datasourceId, newState); + } + } + setActiveDimension(initialActiveDimensionState); + }} + panel={ + <> + {activeGroup && activeId && ( + { + if (shouldReplaceDimension || shouldRemoveDimension) { + props.updateAll( + datasourceId, + newState, + shouldRemoveDimension + ? activeVisualization.removeDimension({ + layerId, + columnId: activeId, + prevState: props.visualizationState, + }) + : activeVisualization.setDimension({ + layerId, + groupId: activeGroup.groupId, + columnId: activeId, + prevState: props.visualizationState, + }) + ); + } else { + props.updateDatasource(datasourceId, newState); + } + setActiveDimension({ + ...activeDimension, + isNew: false, + }); + }, + }} + /> + )} + {activeGroup && + activeId && + !activeDimension.isNew && + activeVisualization.renderDimensionEditor && + activeGroup?.enableDimensionEditor && ( +
+ +
+ )} + + } + /> + ); } diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 740d127e1b08d..344464bfe9590 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -14,6 +14,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiIconTip, EuiSelect, EuiSpacer, EuiSwitch, @@ -57,6 +58,24 @@ const TOGGLE_OFF = i18n.translate('xpack.ml.splom.toggleOff', { const sampleSizeOptions = [100, 1000, 10000].map((d) => ({ value: d, text: '' + d })); +interface OptionLabelWithIconTipProps { + label: string; + tooltip: string; +} + +const OptionLabelWithIconTip: FC = ({ label, tooltip }) => ( + <> + {label} + + +); + export interface ScatterplotMatrixProps { fields: string[]; index: string; @@ -252,9 +271,16 @@ export const ScatterplotMatrix: FC = ({ + } display="rowCompressed" fullWidth > @@ -276,9 +302,16 @@ export const ScatterplotMatrix: FC = ({ + } display="rowCompressed" fullWidth > @@ -292,9 +325,17 @@ export const ScatterplotMatrix: FC = ({ + } display="rowCompressed" fullWidth > @@ -310,9 +351,16 @@ export const ScatterplotMatrix: FC = ({ {resultsField !== undefined && legendType === undefined && ( + } display="rowCompressed" fullWidth > diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts index 0867dc41eeb78..77c263385df0a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts @@ -87,7 +87,7 @@ export const PRIORITY = i18n.translate( export const ALERT_FIELDS_LABEL = i18n.translate( 'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle', { - defaultMessage: 'Fields associated with alerts', + defaultMessage: 'Select Observables to push', } ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx index d82f0769c8b74..fb846d041bd17 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_modal.tsx @@ -123,6 +123,7 @@ export const DeleteActionModal: FC = ({ return ( = ({ closeModal, items, startAndC return ( = ({ }) => { return ( <> - + - +

{stepName}

- + = ({ - + = (item) => { - return {item.name}; + return ( + + {item.name} + + ); }; interface Props { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx index c746a5cc63a9b..9a66b586d1d56 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/legend.tsx @@ -9,18 +9,25 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { IWaterfallContext } from '../context/waterfall_chart'; import { WaterfallChartProps } from './waterfall_chart'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; interface LegendProps { items: Required['legendItems']; render: Required['renderLegendItem']; } +const StyledFlexItem = euiStyled(EuiFlexItem)` + margin-right: ${(props) => props.theme.eui.paddingSizes.m}; + max-width: 7%; + min-width: 160px; +`; + export const Legend: React.FC = ({ items, render }) => { return ( - - {items.map((item, index) => { - return {render(item, index)}; - })} + + {items.map((item, index) => ( + {render(item, index)} + ))} ); }; 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 59990b29db5db..119c907f76ca1 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 @@ -120,8 +120,12 @@ export const WaterfallChart = ({ - - + + {shouldRenderSidebar && } ) { @@ -188,6 +191,40 @@ export function defineRoutes(core: CoreSetup) { } ); + router.put( + { + path: '/api/alerts_fixture/{id}/reset_task_status', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + status: schema.string(), + }), + }, + }, + async ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> => { + const { id } = req.params; + const { status } = req.body; + + const [{ savedObjects }] = await core.getStartServices(); + const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, { + includedHiddenTypes: ['task', 'alert'], + }); + const alert = await savedObjectsWithTasksAndAlerts.get('alert', id); + const result = await savedObjectsWithTasksAndAlerts.update( + 'task', + alert.attributes.scheduledTaskId!, + { status } + ); + return res.ok({ body: result }); + } + ); + router.get( { path: '/api/alerts_fixture/api_keys_pending_invalidation', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts index c1f65fab3669e..e8cc8ea699e17 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/index.ts @@ -11,8 +11,7 @@ import { setupSpacesAndUsers, tearDown } from '..'; // eslint-disable-next-line import/no-default-export export default function alertingTests({ loadTestFile, getService }: FtrProviderContext) { describe('Alerts', () => { - // FLAKY: https://github.com/elastic/kibana/issues/86952 - describe.skip('legacy alerts', () => { + describe('legacy alerts', () => { before(async () => { await setupSpacesAndUsers(getService); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index ef5914965ddce..3db3565374740 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -77,6 +77,7 @@ export default function alertTests({ getService }: FtrProviderContext) { case 'space_1_all at space1': case 'superuser at space1': case 'space_1_all_with_restricted_fixture at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -92,6 +93,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await ensureAlertIsRunning(); break; case 'global_read at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -115,6 +117,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }); break; case 'space_1_all_alerts_none_actions at space1': + await resetTaskStatus(migratedAlertId); await ensureLegacyAlertHasBeenMigrated(migratedAlertId); await updateMigratedAlertToUseApiKeyOfCurrentUser(migratedAlertId); @@ -140,6 +143,21 @@ export default function alertTests({ getService }: FtrProviderContext) { throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`); } + async function resetTaskStatus(alertId: string) { + // occasionally when the task manager starts running while the alert saved objects + // are mid-migration, the task will fail and set its status to "failed". this prevents + // the alert from running ever again and downstream tasks that depend on successful alert + // execution will fail. this ensures the task status is set to "idle" so the + // task manager will continue claiming and executing it. + await supertest + .put(`${getUrlPrefix(space.id)}/api/alerts_fixture/${alertId}/reset_task_status`) + .set('kbn-xsrf', 'foo') + .send({ + status: 'idle', + }) + .expect(200); + } + async function ensureLegacyAlertHasBeenMigrated(alertId: string) { const getResponse = await supertestWithoutAuth .get(`${getUrlPrefix(space.id)}/api/alerts/alert/${alertId}`) diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index d804f0ef14cf8..665c126e00a01 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -159,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); + await transform.table.assertTransformRowActions(testData.originalConfig.id, false); await transform.testExecution.logTestStep('should display the define pivot step'); await transform.table.clickTransformRowAction('Clone'); diff --git a/x-pack/test/functional/apps/transform/deleting.ts b/x-pack/test/functional/apps/transform/deleting.ts new file mode 100644 index 0000000000000..bdba06454c5c2 --- /dev/null +++ b/x-pack/test/functional/apps/transform/deleting.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('deleting', function () { + const PREFIX = 'deleting'; + + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + expected: { + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: 100, + }, + }, + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + expected: { + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'continuous', + progress: undefined, + }, + }, + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX), + transformDescription: 'updated description', + transformDocsPerSecond: '1000', + transformFrequency: '10m', + expected: { + messageText: 'updated transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: 100, + }, + }, + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createAndRunTransform( + testData.originalConfig.id, + testData.originalConfig + ); + } + + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + it('delete transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + if (testData.expected.row.mode === 'continuous') { + await transform.testExecution.logTestStep('should have the delete action disabled'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + false + ); + + await transform.testExecution.logTestStep('should stop the transform'); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Stop' + ); + } + + await transform.testExecution.logTestStep('should display the stopped transform'); + await transform.table.assertTransformRowFields(testData.originalConfig.id, { + id: testData.originalConfig.id, + description: testData.originalConfig.description, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + + await transform.testExecution.logTestStep('should show the delete modal'); + await transform.table.assertTransformRowActionEnabled( + testData.originalConfig.id, + 'Delete', + true + ); + await transform.table.clickTransformRowActionWithRetry( + testData.originalConfig.id, + 'Delete' + ); + await transform.table.assertTransformDeleteModalExists(); + + await transform.testExecution.logTestStep('should delete the transform'); + await transform.table.confirmDeleteTransform(); + await transform.table.assertTransformRowNotExists(testData.originalConfig.id); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 71a7cf02df1fd..1f0bb058bdc38 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -109,7 +109,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); + await transform.table.assertTransformRowActions(testData.originalConfig.id, false); await transform.testExecution.logTestStep('should show the edit flyout'); await transform.table.clickTransformRowAction('Edit'); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 63d8d0b51bc8c..1440f0a3f9a09 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -6,7 +6,10 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform'; +import { + TransformLatestConfig, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -41,6 +44,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./editing')); loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./deleting')); + loadTestFile(require.resolve('./starting')); }); } export interface ComboboxOption { @@ -80,20 +85,46 @@ export function isLatestTransformTestData(arg: any): arg is LatestTransformTestD return arg.type === 'latest'; } -export function getLatestTransformConfig(): TransformLatestConfig { +export function getPivotTransformConfig( + prefix: string, + continuous?: boolean +): TransformPivotConfig { const timestamp = Date.now(); return { - id: `ec_cloning_2_${timestamp}`, + id: `ec_${prefix}_pivot_${timestamp}_${continuous ? 'cont' : 'batch'}`, + source: { index: ['ft_ecommerce'] }, + pivot: { + group_by: { category: { terms: { field: 'category.keyword' } } }, + aggregations: { 'products.base_price.avg': { avg: { field: 'products.base_price' } } }, + }, + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with avg(products.base_price) grouped by terms(category.keyword)`, + dest: { index: `user-ec_2_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), + }; +} + +export function getLatestTransformConfig( + prefix: string, + continuous?: boolean +): TransformLatestConfig { + const timestamp = Date.now(); + return { + id: `ec_${prefix}_latest_${timestamp}_${continuous ? 'cont' : 'batch'}`, source: { index: ['ft_ecommerce'] }, latest: { unique_key: ['category.keyword'], sort: 'order_date', }, - description: 'ecommerce batch transform with category unique key and sorted by order date', + description: `ecommerce ${ + continuous ? 'continuous' : 'batch' + } transform with category unique key and sorted by order date`, frequency: '3s', settings: { max_page_search_size: 250, }, dest: { index: `user-ec_3_${timestamp}` }, + ...(continuous ? { sync: { time: { field: 'order_date', delay: '60s' } } } : {}), }; } diff --git a/x-pack/test/functional/apps/transform/starting.ts b/x-pack/test/functional/apps/transform/starting.ts new file mode 100644 index 0000000000000..4b0b6f8dade66 --- /dev/null +++ b/x-pack/test/functional/apps/transform/starting.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import { getLatestTransformConfig, getPivotTransformConfig } from './index'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const transform = getService('transform'); + + describe('starting', function () { + const PREFIX = 'starting'; + const testDataList = [ + { + suiteTitle: 'batch transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with pivot configuration', + originalConfig: getPivotTransformConfig(PREFIX, true), + mode: 'continuous', + }, + { + suiteTitle: 'batch transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, false), + mode: 'batch', + }, + { + suiteTitle: 'continuous transform with latest configuration', + originalConfig: getLatestTransformConfig(PREFIX, true), + mode: 'continuous', + }, + ]; + + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); + + for (const testData of testDataList) { + await transform.api.createTransform(testData.originalConfig.id, testData.originalConfig); + } + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.securityUI.loginAsTransformPowerUser(); + }); + + after(async () => { + for (const testData of testDataList) { + await transform.testResources.deleteIndexPatternByTitle(testData.originalConfig.dest.index); + await transform.api.deleteIndices(testData.originalConfig.dest.index); + } + + await transform.api.cleanTransformIndices(); + }); + + for (const testData of testDataList) { + const transformId = testData.originalConfig.id; + + describe(`${testData.suiteTitle}`, function () { + it('start transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + await transform.testExecution.logTestStep( + 'should display the original transform in the transform list' + ); + await transform.table.filterWithSearchString(transformId, 1); + + await transform.testExecution.logTestStep('should start the transform'); + await transform.table.assertTransformRowActionEnabled(transformId, 'Start', true); + await transform.table.clickTransformRowActionWithRetry(transformId, 'Start'); + await transform.table.confirmStartTransform(); + await transform.table.clearSearchString(testDataList.length); + + if (testData.mode === 'continuous') { + await transform.testExecution.logTestStep('should display the started transform'); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.STOPPED + ); + } else { + await transform.table.assertTransformRowProgressGreaterThan(transformId, 0); + } + + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.FAILED + ); + await transform.table.assertTransformRowStatusNotEql( + testData.originalConfig.id, + TRANSFORM_STATE.ABORTING + ); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/services/transform/management.ts b/x-pack/test/functional/services/transform/management.ts index fdfd1d1d9b40f..807c3d49e344c 100644 --- a/x-pack/test/functional/services/transform/management.ts +++ b/x-pack/test/functional/services/transform/management.ts @@ -5,8 +5,11 @@ * 2.0. */ +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +export type TransformManagement = ProvidedType; + export function TransformManagementProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 72626580e9461..ce2625677e479 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -12,6 +12,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export function TransformTableProvider({ getService }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return new (class TransformTable { public async parseTransformTable() { @@ -129,21 +130,63 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { const filteredRows = rows.filter((row) => row.id === filter); expect(filteredRows).to.have.length( expectedRowCount, - `Filtered DFA job table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` + `Filtered Transform table should have ${expectedRowCount} row(s) for filter '${filter}' (got matching items '${filteredRows}')` ); } - public async assertTransformRowFields(transformId: string, expectedRow: object) { + public async clearSearchString(expectedRowCount: number = 1) { + await this.waitForTransformsToLoad(); + const tableListContainer = await testSubjects.find('transformListTableContainer'); + const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); + await searchBarInput.clearValueWithKeyboard(); const rows = await this.parseTransformTable(); - const transformRow = rows.filter((row) => row.id === transformId)[0]; - expect(transformRow).to.eql( - expectedRow, - `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( - transformRow - )}')` + expect(rows).to.have.length( + expectedRowCount, + `Transform table should have ${expectedRowCount} row(s) after clearing search' (got '${rows.length}')` ); } + public async assertTransformRowFields(transformId: string, expectedRow: object) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow).to.eql( + expectedRow, + `Expected transform row to be '${JSON.stringify(expectedRow)}' (got '${JSON.stringify( + transformRow + )}')` + ); + }); + } + + public async assertTransformRowProgressGreaterThan( + transformId: string, + expectedProgress: number + ) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.progress).to.greaterThan( + 0, + `Expected transform row progress to be greater than '${expectedProgress}' (got '${transformRow.progress}')` + ); + }); + } + + public async assertTransformRowStatusNotEql(transformId: string, status: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.refreshTransformList(); + const rows = await this.parseTransformTable(); + const transformRow = rows.filter((row) => row.id === transformId)[0]; + expect(transformRow.status).to.not.eql( + status, + `Expected transform row status to not be '${status}' (got '${transformRow.status}')` + ); + }); + } + public async assertTransformExpandedRow() { await testSubjects.click('transformListRowDetailsToggle'); @@ -185,8 +228,13 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { }); } - public async assertTransformRowActions(isTransformRunning = false) { - await testSubjects.click('euiCollapsedItemActionsButton'); + public rowSelector(transformId: string, subSelector?: string) { + const row = `~transformListTable > ~row-${transformId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async assertTransformRowActions(transformId: string, isTransformRunning = false) { + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); await testSubjects.existOrFail('transformActionClone'); await testSubjects.existOrFail('transformActionDelete'); @@ -201,6 +249,42 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { } } + public async assertTransformRowActionEnabled( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit', + expectedValue: boolean + ) { + const selector = `transformAction${action}`; + await retry.tryForTime(60 * 1000, async () => { + await this.refreshTransformList(); + + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + + await testSubjects.existOrFail(selector); + const isEnabled = await testSubjects.isEnabled(selector); + expect(isEnabled).to.eql( + expectedValue, + `Expected '${action}' button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }); + } + + public async clickTransformRowActionWithRetry( + transformId: string, + action: 'Delete' | 'Start' | 'Stop' | 'Clone' | 'Edit' + ) { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + await testSubjects.click(this.rowSelector(transformId, 'euiCollapsedItemActionsButton')); + await testSubjects.existOrFail(`transformAction${action}`); + await testSubjects.click(`transformAction${action}`); + await testSubjects.missingOrFail(`transformAction${action}`); + }); + } + public async clickTransformRowAction(action: string) { await testSubjects.click(`transformAction${action}`); } @@ -214,5 +298,53 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await this.waitForTransformsExpandedRowPreviewTabToLoad(); await this.assertEuiDataGridColumnValues('transformPivotPreview', column, values); } + + public async assertTransformDeleteModalExists() { + await testSubjects.existOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformDeleteModalNotExists() { + await testSubjects.missingOrFail('transformDeleteModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalExists() { + await testSubjects.existOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async assertTransformStartModalNotExists() { + await testSubjects.missingOrFail('transformStartModal', { timeout: 60 * 1000 }); + } + + public async confirmDeleteTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformDeleteModalExists(); + await testSubjects.click('transformDeleteModal > confirmModalConfirmButton'); + await this.assertTransformDeleteModalNotExists(); + }); + } + + public async assertTransformRowNotExists(transformId: string) { + await retry.tryForTime(30 * 1000, async () => { + // If after deletion, and there's no transform left + const noTransformsFoundMessageExists = await testSubjects.exists( + 'transformNoTransformsFound' + ); + + if (noTransformsFoundMessageExists) { + return true; + } else { + // Checks that the tranform was deleted + await this.filterWithSearchString(transformId, 0); + } + }); + } + + public async confirmStartTransform() { + await retry.tryForTime(30 * 1000, async () => { + await this.assertTransformStartModalExists(); + await testSubjects.click('transformStartModal > confirmModalConfirmButton'); + await this.assertTransformStartModalNotExists(); + }); + } })(); } diff --git a/yarn.lock b/yarn.lock index bd95cec520a4c..6cd37aa545577 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20000,10 +20000,10 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.2.0, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.19, lodash@~4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== log-ok@^0.1.1: version "0.1.1"