diff --git a/.ci/Jenkinsfile_security_cypress b/.ci/Jenkinsfile_security_cypress index 811af44d1ca56..f7b02cd1c4ab1 100644 --- a/.ci/Jenkinsfile_security_cypress +++ b/.ci/Jenkinsfile_security_cypress @@ -17,9 +17,7 @@ kibanaPipeline(timeoutMinutes: 180) { workers.ci(name: job, size: 'l', ramDisk: true) { kibanaPipeline.bash('test/scripts/jenkins_xpack_build_kibana.sh', 'Build Default Distributable') - kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_chrome.sh')() - // Temporarily disabled to figure out test flake - // kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress_firefox.sh')() + kibanaPipeline.functionalTestProcess(job, 'test/scripts/jenkins_security_solution_cypress.sh')() } } } 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/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index c3185aaad553a..cadf8e0b16a44 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -105,7 +105,7 @@ See <> for how to obtain the endpoint and + * The action’s type: Trigger, Resolve, or Acknowledge. * The event’s severity: Info, warning, error, or critical. -* An array of different fields, including the timestamp, group, class, component, and your dedup key. +* An array of different fields, including the timestamp, group, class, component, and your dedup key. By default, the dedup is configured to create a new PagerDuty incident for each alert instance and reuse the incident when a recovered alert instance reactivates. Depending on your custom needs, assign them variables from the alerting context. To see the available context variables, click on the *Add alert variable* icon next to each corresponding field. For more details on these parameters, see the @@ -179,7 +179,7 @@ PagerDuty actions have the following properties: Severity:: The perceived severity of on the affected system. This can be one of `Critical`, `Error`, `Warning` or `Info`(default). Event action:: One of `Trigger` (default), `Resolve`, or `Acknowledge`. See https://v2.developer.pagerduty.com/docs/events-api-v2#event-action[event action] for more details. -Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if unset defaults to `action:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. +Dedup Key:: All actions sharing this key will be associated with the same PagerDuty alert. This value is used to correlate trigger and resolution. This value is *optional*, and if not set, defaults to `:`. The maximum length is *255* characters. See https://v2.developer.pagerduty.com/docs/events-api-v2#alert-de-duplication[alert deduplication] for details. Timestamp:: An *optional* https://v2.developer.pagerduty.com/v2/docs/types#datetime[ISO-8601 format date-time], indicating the time the event was detected or generated. Component:: An *optional* value indicating the component of the source machine that is responsible for the event, for example `mysql` or `eth0`. Group:: An *optional* value indicating the logical grouping of components of a service, for example `app-stack`. diff --git a/src/plugins/embeddable/README.asciidoc b/src/plugins/embeddable/README.asciidoc index 007b16587e9f8..165dc37c56cb3 100644 --- a/src/plugins/embeddable/README.asciidoc +++ b/src/plugins/embeddable/README.asciidoc @@ -26,7 +26,7 @@ link:https://github.com/elastic/kibana/blob/master/src/plugins/embeddable/docs/R === API docs -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddablestart.md[Browser Start contract] diff --git a/src/plugins/expressions/README.asciidoc b/src/plugins/expressions/README.asciidoc index e07f6e2909ab8..554e3bfcb6976 100644 --- a/src/plugins/expressions/README.asciidoc +++ b/src/plugins/expressions/README.asciidoc @@ -46,7 +46,7 @@ image::https://user-images.githubusercontent.com/9773803/74162514-3250a880-4c21- https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserversetup.md[Server Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionsserverstart.md[Server Start contract] -===== Browser API +==== Browser API https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsservicesetup.md[Browser Setup contract] https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionsstart.md[Browser Start contract] diff --git a/src/plugins/vis_type_xy/server/plugin.ts b/src/plugins/vis_type_xy/server/plugin.ts index fd670e288ff5b..a9e6020cf3ee8 100644 --- a/src/plugins/vis_type_xy/server/plugin.ts +++ b/src/plugins/vis_type_xy/server/plugin.ts @@ -20,6 +20,7 @@ export const uiSettingsConfig: Record> = { name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', { defaultMessage: 'Legacy charts library', }), + requiresPageReload: true, value: false, description: i18n.translate( 'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description', diff --git a/vars/tasks.groovy b/vars/tasks.groovy index a61035403dabd..24ea5f9c8c887 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -126,9 +126,7 @@ def functionalXpack(Map params = [:]) { 'x-pack/plugins/triggers_actions_ui/public/application/context/actions_connectors_context.tsx', ]) { if (githubPr.isPr()) { - task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypressChrome', './test/scripts/jenkins_security_solution_cypress_chrome.sh')) - // Temporarily disabled to figure out test flake - // task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypressFirefox', './test/scripts/jenkins_security_solution_cypress_firefox.sh')) + task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')) } } } diff --git a/x-pack/plugins/event_log/README.md b/x-pack/plugins/event_log/README.md index eb7fbc9d590fa..9c806680f68a2 100644 --- a/x-pack/plugins/event_log/README.md +++ b/x-pack/plugins/event_log/README.md @@ -164,10 +164,12 @@ history records associated with specific saved object ids. ## API +Event Log plugin returns a service instance from setup() and client service from start() methods. + +### Setup ```typescript // IEvent is a TS type generated from the subset of ECS supported -// the NP plugin returns a service instance from setup() and start() export interface IEventLogService { registerProviderActions(provider: string, actions: string[]): void; isProviderActionRegistered(provider: string, action: string): boolean; @@ -237,6 +239,80 @@ properties `start`, `end`, and `duration` in the event. For example: It's anticipated that more "helper" methods like this will be provided in the future. +### Start +```typescript + +export interface IEventLogClientService { + getClient(request: KibanaRequest): IEventLogClient; +} + +export interface IEventLogClient { + findEventsBySavedObjectIds( + type: string, + ids: string[], + options?: Partial + ): Promise; +} +``` + +The plugin exposes an `IEventLogClientService` object to plugins that request it. +These plugins must call `getClient(request)` to get the event log client. + +## Experimental RESTful API + +Usage of the event log allows you to retrieve the events for a given saved object type by the specified set of IDs. +The following API is experimental and can change or be removed in a future release. + +### `GET /api/event_log/{type}/{id}/_find`: Get events for a given saved object type by the ID + +Collects event information from the event log for the selected saved object by type and ID. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| +|id|The id of the saved object.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event fields returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +### `POST /api/event_log/{type}/_find`: Retrive events for a given saved object type by the IDs + +Collects event information from the event log for the selected saved object by type and by IDs. + +Params: + +|Property|Description|Type| +|---|---|---| +|type|The type of the saved object whose events you're trying to get.|string| + +Query: + +|Property|Description|Type| +|---|---|---| +|page|The page number.|number| +|per_page|The number of events to return per page.|number| +|sort_field|Sorts the response. Could be an event field returned in the response.|string| +|sort_order|Sort direction, either `asc` or `desc`.|string| +|filter|A KQL string that you filter with an attribute from the event. It should look like `event.action:(execute)`.|string| +|start|The date to start looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| +|end|The date to stop looking for saved object events in the event log. Either an ISO date string, or a duration string that indicates the time since now.|string| + +Body: + +|Property|Description|Type| +|---|---|---| +|ids|The array ids of the saved object.|string array| ## Stored data @@ -303,4 +379,3 @@ For more relevant information on ILM, see: [getting started with ILM doc]: https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index-lifecycle-management.html [write index alias behavior]: https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-rollover-index.html#indices-rollover-is-write-index - diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index a9845c2315604..c61b431eed46d 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -183,6 +183,13 @@ export const setup = async (arg?: { const enable = (phase: Phases) => createFormToggleAction(`enablePhaseSwitch-${phase}`); + const showDataAllocationOptions = (phase: Phases) => () => { + act(() => { + find(`${phase}-dataTierAllocationControls.dataTierSelect`).simulate('click'); + }); + component.update(); + }; + const createMinAgeActions = (phase: Phases) => { return { hasMinAgeInput: () => exists(`${phase}-selectedMinimumAge`), @@ -222,12 +229,10 @@ export const setup = async (arg?: { const createSearchableSnapshotActions = (phase: Phases) => { const fieldSelector = `searchableSnapshotField-${phase}`; const licenseCalloutSelector = `${fieldSelector}.searchableSnapshotDisabledDueToLicense`; - const rolloverCalloutSelector = `${fieldSelector}.searchableSnapshotFieldsNoRolloverCallout`; const toggleSelector = `${fieldSelector}.searchableSnapshotToggle`; const toggleSearchableSnapshot = createFormToggleAction(toggleSelector); return { - searchableSnapshotDisabledDueToRollover: () => exists(rolloverCalloutSelector), searchableSnapshotDisabled: () => exists(licenseCalloutSelector) && find(licenseCalloutSelector).props().disabled === true, searchableSnapshotsExists: () => exists(fieldSelector), @@ -379,6 +384,7 @@ export const setup = async (arg?: { }, warm: { enable: enable('warm'), + showDataAllocationOptions: showDataAllocationOptions('warm'), ...createMinAgeActions('warm'), setReplicas: setReplicas('warm'), hasErrorIndicator: () => exists('phaseErrorIndicator-warm'), @@ -390,6 +396,7 @@ export const setup = async (arg?: { }, cold: { enable: enable('cold'), + showDataAllocationOptions: showDataAllocationOptions('cold'), ...createMinAgeActions('cold'), setReplicas: setReplicas('cold'), setFreeze, diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index 7fe5c6f50d046..740aeebb852f1 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -405,6 +405,7 @@ describe('', () => { await actions.cold.setMinAgeUnits('s'); await actions.cold.setDataAllocation('node_attrs'); await actions.cold.setSelectedNodeAttribute('test:123'); + await actions.cold.setSearchableSnapshot('my-repo'); await actions.cold.setReplicas('123'); await actions.cold.setFreeze(true); await actions.cold.setIndexPriority('123'); @@ -426,6 +427,9 @@ describe('', () => { }, }, "freeze": Object {}, + "searchable_snapshot": Object { + "snapshot_repository": "my-repo", + }, "set_priority": Object { "priority": 123, }, @@ -445,19 +449,6 @@ describe('', () => { } `); }); - - // Setting searchable snapshot field disables setting replicas so we test this separately - test('setting searchable snapshot', async () => { - const { actions } = testBed; - await actions.cold.enable(true); - await actions.cold.setSearchableSnapshot('my-repo'); - await actions.savePolicy(); - const latestRequest2 = server.requests[server.requests.length - 1]; - const entirePolicy2 = JSON.parse(JSON.parse(latestRequest2.requestBody).body); - expect(entirePolicy2.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'my-repo' - ); - }); }); }); @@ -693,43 +684,52 @@ describe('', () => { expect(find('cold-dataTierAllocationControls.dataTierSelect').text()).toContain('Off'); }); }); - }); - - describe('searchable snapshot', () => { describe('on cloud', () => { - describe('new policy', () => { + describe('using legacy data role config', () => { beforeEach(async () => { - // simulate creating a new policy - httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + // On cloud, even if there are data_* roles set, the default, recommended allocation option should not + // be available. + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: true, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); await act(async () => { - testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + testBed = await setup({ + appServicesContext: { + cloud: { + isCloudEnabled: true, + }, + license: licensingMock.createLicense({ license: { type: 'basic' } }), + }, + }); }); const { component } = testBed; component.update(); }); - test('defaults searchable snapshot to true on cloud', async () => { - const { find, actions } = testBed; - await actions.cold.enable(true); - expect( - find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] - ).toBe(true); + test('removes default, recommended option', async () => { + const { actions, find } = testBed; + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + + expect(find('defaultDataAllocationOption').exists()).toBeFalsy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // Show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeTruthy(); }); }); - describe('existing policy', () => { + describe('using node roles', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); httpRequestsMockHelpers.setListNodes({ - isUsingDeprecatedDataRoleConfig: false, nodesByAttributes: { test: ['123'] }, - nodesByRoles: { data: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, }); httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); @@ -740,19 +740,34 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('correctly sets snapshot repository default to "found-snapshots"', async () => { - const { actions } = testBed; + + test('should show recommended, custom and "off" options on cloud with data roles', async () => { + const { actions, find } = testBed; + + await actions.warm.enable(true); + actions.warm.showDataAllocationOptions(); + expect(find('defaultDataAllocationOption').exists()).toBeTruthy(); + expect(find('customDataAllocationOption').exists()).toBeTruthy(); + expect(find('noneDataAllocationOption').exists()).toBeTruthy(); + // We should not be showing the call-to-action for users to activate the cold tier on cloud + expect(find('cloudMissingColdTierCallout').exists()).toBeFalsy(); + // Do not show the call-to-action for users to migrate their cluster to use node roles + expect(find('cloudDataTierCallout').exists()).toBeFalsy(); + }); + + test('should show cloud notice when cold tier nodes do not exist', async () => { + const { actions, find } = testBed; await actions.cold.enable(true); - await actions.cold.toggleSearchableSnapshot(true); - await actions.savePolicy(); - const latestRequest = server.requests[server.requests.length - 1]; - const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); - expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( - 'found-snapshots' - ); + expect(find('cloudMissingColdTierCallout').exists()).toBeTruthy(); + // Assert that other notices are not showing + expect(find('defaultAllocationNotice').exists()).toBeFalsy(); + expect(find('noNodeAttributesWarning').exists()).toBeFalsy(); }); }); }); + }); + + describe('searchable snapshot', () => { describe('on non-enterprise license', () => { beforeEach(async () => { httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); @@ -789,6 +804,64 @@ describe('', () => { expect(actions.cold.searchableSnapshotDisabledDueToLicense()).toBeTruthy(); }); }); + + describe('on cloud', () => { + describe('new policy', () => { + beforeEach(async () => { + // simulate creating a new policy + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('')]); + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('defaults searchable snapshot to true on cloud', async () => { + const { find, actions } = testBed; + await actions.cold.enable(true); + expect( + find('searchableSnapshotField-cold.searchableSnapshotToggle').props()['aria-checked'] + ).toBe(true); + }); + }); + describe('existing policy', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setLoadPolicies([getDefaultHotPhasePolicy('my_policy')]); + httpRequestsMockHelpers.setListNodes({ + isUsingDeprecatedDataRoleConfig: false, + nodesByAttributes: { test: ['123'] }, + nodesByRoles: { data_hot: ['123'] }, + }); + httpRequestsMockHelpers.setListSnapshotRepos({ repositories: ['found-snapshots'] }); + + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + + const { component } = testBed; + component.update(); + }); + test('correctly sets snapshot repository default to "found-snapshots"', async () => { + const { actions } = testBed; + await actions.cold.enable(true); + await actions.cold.toggleSearchableSnapshot(true); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const request = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(request.phases.cold.actions.searchable_snapshot.snapshot_repository).toEqual( + 'found-snapshots' + ); + }); + }); + }); }); describe('with rollover', () => { beforeEach(async () => { @@ -844,14 +917,15 @@ describe('', () => { const { component } = testBed; component.update(); }); - test('hiding and disabling searchable snapshot field', async () => { + test('hides fields in hot phase', async () => { const { actions } = testBed; await actions.hot.toggleDefaultRollover(false); await actions.hot.toggleRollover(false); - await actions.cold.enable(true); + expect(actions.hot.forceMergeFieldExists()).toBeFalsy(); + expect(actions.hot.shrinkExists()).toBeFalsy(); expect(actions.hot.searchableSnapshotsExists()).toBeFalsy(); - expect(actions.cold.searchableSnapshotDisabledDueToRollover()).toBeTruthy(); + expect(actions.hot.readonlyExists()).toBeFalsy(); }); test('hiding rollover tip on minimum age', async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts index 113698fdf6df2..b02d190d10899 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/reactive_form/node_allocation.test.ts @@ -125,7 +125,7 @@ describe(' node allocation', () => { expect(actions.warm.hasDefaultAllocationWarning()).toBeTruthy(); }); - test('shows default allocation notice when hot tier exists, but not warm tier', async () => { + test('when configuring warm phase shows default allocation notice when hot tier exists, but not warm tier', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: {}, nodesByRoles: { data_hot: ['test'], data_cold: ['test'] }, @@ -309,7 +309,7 @@ describe(' node allocation', () => { describe('on cloud', () => { describe('with deprecated data role config', () => { - test('should hide data tier option on cloud using legacy node role configuration', async () => { + test('should hide data tier option on cloud', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, // On cloud, if using legacy config there will not be any "data_*" roles set. @@ -331,10 +331,29 @@ describe(' node allocation', () => { expect(exists('customDataAllocationOption')).toBeTruthy(); expect(exists('noneDataAllocationOption')).toBeTruthy(); }); + + test('should ask users to migrate to node roles when on cloud using legacy data role', async () => { + httpRequestsMockHelpers.setListNodes({ + nodesByAttributes: { test: ['123'] }, + // On cloud, if using legacy config there will not be any "data_*" roles set. + nodesByRoles: { data: ['test'] }, + isUsingDeprecatedDataRoleConfig: true, + }); + await act(async () => { + testBed = await setup({ appServicesContext: { cloud: { isCloudEnabled: true } } }); + }); + const { actions, component, exists } = testBed; + + component.update(); + await actions.warm.enable(true); + expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); + + expect(exists('cloudDataTierCallout')).toBeTruthy(); + }); }); describe('with node role config', () => { - test('shows off, custom and data role options on cloud with data roles', async () => { + test('shows data role, custom and "off" options on cloud with data roles', async () => { httpRequestsMockHelpers.setListNodes({ nodesByAttributes: { test: ['123'] }, nodesByRoles: { data: ['test'], data_hot: ['test'], data_warm: ['test'] }, @@ -372,7 +391,7 @@ describe(' node allocation', () => { await actions.cold.enable(true); expect(component.find('.euiLoadingSpinner').exists()).toBeFalsy(); - expect(exists('cloudDataTierCallout')).toBeTruthy(); + expect(exists('cloudMissingColdTierCallout')).toBeTruthy(); // Assert that other notices are not showing expect(actions.cold.hasDefaultAllocationNotice()).toBeFalsy(); expect(actions.cold.hasNoNodeAttrsWarning()).toBeFalsy(); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx index 27aacef1a368b..1dbc30674eaa5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/cold_phase/cold_phase.tsx @@ -8,12 +8,9 @@ import React, { FunctionComponent } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; import { EuiTextColor } from '@elastic/eui'; -import { useFormData } from '../../../../../../shared_imports'; - import { useConfigurationIssues } from '../../../form'; import { LearnMoreLink, ToggleFieldWithDescribedFormRow } from '../../'; @@ -36,23 +33,12 @@ const i18nTexts = { }, }; -const formFieldPaths = { - enabled: '_meta.cold.enabled', - searchableSnapshot: 'phases.cold.actions.searchable_snapshot.snapshot_repository', -}; - export const ColdPhase: FunctionComponent = () => { const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); - const [formData] = useFormData({ - watch: [formFieldPaths.searchableSnapshot], - }); - - const showReplicasField = get(formData, formFieldPaths.searchableSnapshot) == null; - return ( }> - {showReplicasField && } + {/* Freeze section */} {!isUsingSearchableSnapshotInHotPhase && ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx index 4d3dbbba39037..351d6ac1c530b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/cloud_data_tier_callout.tsx @@ -7,21 +7,38 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiCallOut } from '@elastic/eui'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; const i18nTexts = { title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.title', { - defaultMessage: 'Create a cold tier', + defaultMessage: 'Migrate to data tiers', }), body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.body', { - defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + defaultMessage: 'Migrate your Elastic Cloud deployment to use data tiers.', }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudDataTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), }; -export const CloudDataTierCallout: FunctionComponent = () => { +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to migrate to data tiers if their cluster is still running + * the deprecated node.data:true config. + */ +export const CloudDataTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { return ( - {i18nTexts.body} + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx index 562267089051a..e43b750849774 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_notice.tsx @@ -11,8 +11,6 @@ import { EuiCallOut } from '@elastic/eui'; import { PhaseWithAllocation, DataTierRole } from '../../../../../../../../../common/types'; -import { AllocationNodeRole } from '../../../../../../../lib'; - const i18nTextsNodeRoleToDataTier: Record = { data_hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.dataTierHotLabel', { defaultMessage: 'hot', @@ -84,24 +82,13 @@ const i18nTexts = { interface Props { phase: PhaseWithAllocation; - targetNodeRole: AllocationNodeRole; + targetNodeRole: DataTierRole; } export const DefaultAllocationNotice: FunctionComponent = ({ phase, targetNodeRole }) => { - const content = - targetNodeRole === 'none' ? ( - - {i18nTexts.warning[phase].body} - - ) : ( - - {i18nTexts.notice[phase].body(targetNodeRole)} - - ); - - return content; + return ( + + {i18nTexts.notice[phase].body(targetNodeRole)} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx new file mode 100644 index 0000000000000..a194f3c07f900 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/default_allocation_warning.tsx @@ -0,0 +1,59 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut } from '@elastic/eui'; + +import { PhaseWithAllocation } from '../../../../../../../../../common/types'; + +const i18nTexts = { + warning: { + warm: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the warm tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.warmPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the warm or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + cold: { + title: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableTitle', + { defaultMessage: 'No nodes assigned to the cold tier' } + ), + body: i18n.translate( + 'xpack.indexLifecycleMgmt.coldPhase.dataTier.defaultAllocationNotAvailableBody', + { + defaultMessage: + 'Assign at least one node to the cold, warm, or hot tier to use role-based allocation. The policy will fail to complete allocation if there are no available nodes.', + } + ), + }, + }, +}; + +interface Props { + phase: PhaseWithAllocation; +} + +export const DefaultAllocationWarning: FunctionComponent = ({ phase }) => { + return ( + + {i18nTexts.warning[phase].body} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts index b3f57ac24e0d7..938e0a850f933 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/index.ts @@ -13,8 +13,12 @@ export { DataTierAllocation } from './data_tier_allocation'; export { DefaultAllocationNotice } from './default_allocation_notice'; +export { DefaultAllocationWarning } from './default_allocation_warning'; + export { NoNodeAttributesWarning } from './no_node_attributes_warning'; +export { MissingColdTierCallout } from './missing_cold_tier_callout'; + export { CloudDataTierCallout } from './cloud_data_tier_callout'; export { LoadingError } from './loading_error'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx new file mode 100644 index 0000000000000..21b8850e0b088 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/components/missing_cold_tier_callout.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.title', { + defaultMessage: 'Create a cold tier', + }), + body: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.body', { + defaultMessage: 'Edit your Elastic Cloud deployment to set up a cold tier.', + }), + linkText: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.cloudMissingColdTierCallout.linkToCloudDeploymentDescription', + { defaultMessage: 'View cloud deployment' } + ), +}; + +interface Props { + linkToCloudDeployment?: string; +} + +/** + * A call-to-action for users to activate their cold tier slider to provision cold tier nodes. + * This may need to be change when we have autoscaling enabled on a cluster because nodes may not + * yet exist, but will automatically be provisioned. + */ +export const MissingColdTierCallout: FunctionComponent = ({ linkToCloudDeployment }) => { + return ( + + {i18nTexts.body}{' '} + {Boolean(linkToCloudDeployment) && ( + + {i18nTexts.linkText} + + )} + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx index ad36039728f5c..7a660e0379a8d 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/data_tier_allocation_field/data_tier_allocation_field.tsx @@ -14,9 +14,7 @@ import { useKibana, useFormData } from '../../../../../../../shared_imports'; import { PhaseWithAllocation } from '../../../../../../../../common/types'; -import { getAvailableNodeRoleForPhase } from '../../../../../../lib/data_tiers'; - -import { isNodeRoleFirstPreference } from '../../../../../../lib'; +import { getAvailableNodeRoleForPhase, isNodeRoleFirstPreference } from '../../../../../../lib'; import { useLoadNodes } from '../../../../../../services/api'; @@ -25,7 +23,9 @@ import { DataTierAllocationType } from '../../../../types'; import { DataTierAllocation, DefaultAllocationNotice, + DefaultAllocationWarning, NoNodeAttributesWarning, + MissingColdTierCallout, CloudDataTierCallout, LoadingError, } from './components'; @@ -65,30 +65,48 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); const hasNodeAttrs = Boolean(Object.keys(nodesByAttributes ?? {}).length); const isCloudEnabled = cloud?.isCloudEnabled ?? false; + const cloudDeploymentUrl = cloud?.cloudDeploymentUrl; const renderNotice = () => { switch (allocationType) { case 'node_roles': - if (isCloudEnabled && phase === 'cold') { - const isUsingNodeRolesAllocation = !isUsingDeprecatedDataRoleConfig && hasDataNodeRoles; + /** + * We'll drive Cloud users to add a cold tier to their deployment if there are no nodes with the cold node role. + */ + if (isCloudEnabled && phase === 'cold' && !isUsingDeprecatedDataRoleConfig) { const hasNoNodesWithNodeRole = !nodesByRoles.data_cold?.length; - if (isUsingNodeRolesAllocation && hasNoNodesWithNodeRole) { + if (hasDataNodeRoles && hasNoNodesWithNodeRole) { // Tell cloud users they can deploy nodes on cloud. return ( <> - + ); } } + /** + * Node role allocation moves data in a phase to a corresponding tier of the same name. To prevent policy execution from getting + * stuck ILM allocation will fall back to a previous tier if possible. We show the WARNING below to inform a user when even + * this fallback will not succeed. + */ const allocationNodeRole = getAvailableNodeRoleForPhase(phase, nodesByRoles); - if ( - allocationNodeRole === 'none' || - !isNodeRoleFirstPreference(phase, allocationNodeRole) - ) { + if (allocationNodeRole === 'none') { + return ( + <> + + + + ); + } + + /** + * If we are able to fallback to a data tier that does not map to this phase, we show a notice informing the user that their + * data will not be assigned to a corresponding tier. + */ + if (!isNodeRoleFirstPreference(phase, allocationNodeRole)) { return ( <> @@ -106,6 +124,19 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr ); } + /** + * Special cloud case: when deprecated data role configuration is in use, it means that this deployment is not using + * the new node role based allocation. We drive users to the cloud console to migrate to node role based allocation + * in that case. + */ + if (isCloudEnabled && isUsingDeprecatedDataRoleConfig) { + return ( + <> + + + + ); + } break; default: return null; @@ -141,9 +172,7 @@ export const DataTierAllocationField: FunctionComponent = ({ phase, descr hasNodeAttributes={hasNodeAttrs} phase={phase} nodes={nodesByAttributes} - disableDataTierOption={Boolean( - isCloudEnabled && !hasDataNodeRoles && isUsingDeprecatedDataRoleConfig - )} + disableDataTierOption={Boolean(isCloudEnabled && isUsingDeprecatedDataRoleConfig)} isLoading={isLoading} /> diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 97e7d0bcc27de..1a78149521e63 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -52,7 +52,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => services: { cloud }, } = useKibana(); const { getUrlForApp, policy, license, isNewPolicy } = useEditPolicyContext(); - const { isUsingSearchableSnapshotInHotPhase, isUsingRollover } = useConfigurationIssues(); + const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); const searchableSnapshotPath = `phases.${phase}.actions.searchable_snapshot.snapshot_repository`; @@ -62,10 +62,8 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => const isColdPhase = phase === 'cold'; const isDisabledDueToLicense = !license.canUseSearchableSnapshot(); const isDisabledInColdDueToHotPhase = isColdPhase && isUsingSearchableSnapshotInHotPhase; - const isDisabledInColdDueToRollover = isColdPhase && !isUsingRollover; - const isDisabled = - isDisabledDueToLicense || isDisabledInColdDueToHotPhase || isDisabledInColdDueToRollover; + const isDisabled = isDisabledDueToLicense || isDisabledInColdDueToHotPhase; const [isFieldToggleChecked, setIsFieldToggleChecked] = useState(() => Boolean( @@ -294,20 +292,6 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => )} /> ); - } else if (isDisabledInColdDueToRollover) { - infoCallout = ( - - ); } return infoCallout ? ( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx index e5bf34890a4a7..577dab6804147 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/warm_phase/warm_phase.tsx @@ -33,22 +33,22 @@ export const WarmPhase: FunctionComponent = () => { const { isUsingSearchableSnapshotInHotPhase } = useConfigurationIssues(); return ( - - + + - {!isUsingSearchableSnapshotInHotPhase && } + {!isUsingSearchableSnapshotInHotPhase && } - {!isUsingSearchableSnapshotInHotPhase && } + {!isUsingSearchableSnapshotInHotPhase && } - + {/* Data tier allocation section */} - + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/types.ts b/x-pack/plugins/index_lifecycle_management/public/types.ts index 27b3795c6731f..adfca9ad41b26 100644 --- a/x-pack/plugins/index_lifecycle_management/public/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/types.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; import { ManagementSetup } from '../../../../src/plugins/management/public'; diff --git a/x-pack/plugins/maps/common/migrations/references.js b/x-pack/plugins/maps/common/migrations/references.js deleted file mode 100644 index ab8edbefb27c2..0000000000000 --- a/x-pack/plugins/maps/common/migrations/references.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// Can not use public Layer classes to extract references since this logic must run in both client and server. - -import _ from 'lodash'; -import { SOURCE_TYPES } from '../constants'; - -function doesSourceUseIndexPattern(layerDescriptor) { - const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); - return ( - sourceType === SOURCE_TYPES.ES_GEO_GRID || - sourceType === SOURCE_TYPES.ES_SEARCH || - sourceType === SOURCE_TYPES.ES_PEW_PEW - ); -} - -export function extractReferences({ attributes, references = [] }) { - if (!attributes.layerListJSON) { - return { attributes, references }; - } - - const extractedReferences = []; - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer, layerIndex) => { - // Extract index-pattern references from source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternId')) { - const refName = `layer_${layerIndex}_source_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: layer.sourceDescriptor.indexPatternId, - }); - delete layer.sourceDescriptor.indexPatternId; - layer.sourceDescriptor.indexPatternRefName = refName; - } - - // Extract index-pattern references from join - const joins = _.get(layer, 'joins', []); - joins.forEach((join, joinIndex) => { - if (_.has(join, 'right.indexPatternId')) { - const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; - extractedReferences.push({ - name: refName, - type: 'index-pattern', - id: join.right.indexPatternId, - }); - delete join.right.indexPatternId; - join.right.indexPatternRefName = refName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - references: references.concat(extractedReferences), - }; -} - -function findReference(targetName, references) { - const reference = references.find((reference) => reference.name === targetName); - if (!reference) { - throw new Error(`Could not find reference "${targetName}"`); - } - return reference; -} - -export function injectReferences({ attributes, references }) { - if (!attributes.layerListJSON) { - return { attributes }; - } - - const layerList = JSON.parse(attributes.layerListJSON); - layerList.forEach((layer) => { - // Inject index-pattern references into source descriptor - if (doesSourceUseIndexPattern(layer) && _.has(layer, 'sourceDescriptor.indexPatternRefName')) { - const reference = findReference(layer.sourceDescriptor.indexPatternRefName, references); - layer.sourceDescriptor.indexPatternId = reference.id; - delete layer.sourceDescriptor.indexPatternRefName; - } - - // Inject index-pattern references into join - const joins = _.get(layer, 'joins', []); - joins.forEach((join) => { - if (_.has(join, 'right.indexPatternRefName')) { - const reference = findReference(join.right.indexPatternRefName, references); - join.right.indexPatternId = reference.id; - delete join.right.indexPatternRefName; - } - }); - }); - - return { - attributes: { - ...attributes, - layerListJSON: JSON.stringify(layerList), - }, - }; -} diff --git a/x-pack/plugins/maps/common/migrations/references.test.js b/x-pack/plugins/maps/common/migrations/references.test.ts similarity index 98% rename from x-pack/plugins/maps/common/migrations/references.test.js rename to x-pack/plugins/maps/common/migrations/references.test.ts index 3b8b7de441be4..5b749022bb62b 100644 --- a/x-pack/plugins/maps/common/migrations/references.test.js +++ b/x-pack/plugins/maps/common/migrations/references.test.ts @@ -128,7 +128,7 @@ describe('injectReferences', () => { const attributes = { title: 'my map', }; - expect(injectReferences({ attributes })).toEqual({ + expect(injectReferences({ attributes, references: [] })).toEqual({ attributes: { title: 'my map', }, diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts new file mode 100644 index 0000000000000..d48be6bd56fbe --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -0,0 +1,120 @@ +/* + * 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. + */ + +// Can not use public Layer classes to extract references since this logic must run in both client and server. + +import { SavedObjectReference } from '../../../../../src/core/types'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; +import { LayerDescriptor } from '../descriptor_types'; + +interface IndexPatternReferenceDescriptor { + indexPatternId?: string; + indexPatternRefName?: string; +} + +export function extractReferences({ + attributes, + references = [], +}: { + attributes: MapSavedObjectAttributes; + references?: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes, references }; + } + + const extractedReferences: SavedObjectReference[] = []; + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer, layerIndex) => { + // Extract index-pattern references from source descriptor + if (layer.sourceDescriptor && 'indexPatternId' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_source_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + + // Extract index-pattern references from join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join, joinIndex) => { + if ('indexPatternId' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const refName = `layer_${layerIndex}_join_${joinIndex}_index_pattern`; + extractedReferences.push({ + name: refName, + type: 'index-pattern', + id: sourceDescriptor.indexPatternId!, + }); + delete sourceDescriptor.indexPatternId; + sourceDescriptor.indexPatternRefName = refName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + references: references.concat(extractedReferences), + }; +} + +function findReference(targetName: string, references: SavedObjectReference[]) { + const reference = references.find(({ name }) => name === targetName); + if (!reference) { + throw new Error(`Could not find reference "${targetName}"`); + } + return reference; +} + +export function injectReferences({ + attributes, + references, +}: { + attributes: MapSavedObjectAttributes; + references: SavedObjectReference[]; +}) { + if (!attributes.layerListJSON) { + return { attributes }; + } + + const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + layerList.forEach((layer) => { + // Inject index-pattern references into source descriptor + if (layer.sourceDescriptor && 'indexPatternRefName' in layer.sourceDescriptor) { + const sourceDescriptor = layer.sourceDescriptor as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + + // Inject index-pattern references into join + const joins = layer.joins ? layer.joins : []; + joins.forEach((join) => { + if ('indexPatternRefName' in join.right) { + const sourceDescriptor = join.right as IndexPatternReferenceDescriptor; + const reference = findReference(sourceDescriptor.indexPatternRefName!, references); + sourceDescriptor.indexPatternId = reference.id; + delete sourceDescriptor.indexPatternRefName; + } + }); + }); + + return { + attributes: { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }, + }; +} diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 7e15bfa9a340e..b1944f8136709 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -16,7 +16,6 @@ import { MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; import { getMapEmbeddableDisplayName } from '../../common/i18n_getters'; import { MapByReferenceInput, MapEmbeddableInput, MapByValueInput } from './types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; -// @ts-expect-error import { extractReferences } from '../../common/migrations/references'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { @@ -69,7 +68,9 @@ export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { const maybeMapByValueInput = state as EmbeddableStateWithType | MapByValueInput; if ((maybeMapByValueInput as MapByValueInput).attributes !== undefined) { - const { references } = extractReferences(maybeMapByValueInput); + const { references } = extractReferences({ + attributes: (maybeMapByValueInput as MapByValueInput).attributes, + }); return { state, references }; } diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 71b0b7f28a2ff..5f7c45b1b42d7 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -12,7 +12,6 @@ import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; import { getMapEmbeddableDisplayName } from '../common/i18n_getters'; import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/saved_objects/public'; import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './kibana_services'; -// @ts-expect-error import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 8f9b529ae30f5..bf180c514c56f 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -24,7 +24,6 @@ import { import { MapSavedObject, MapSavedObjectAttributes } from '../../common/map_saved_object_type'; import { getIndexPatternsService, getInternalRepository } from '../kibana_server_services'; import { MapsConfigType } from '../../config'; -// @ts-expect-error import { injectReferences } from '././../../common/migrations/references'; interface Settings { @@ -314,7 +313,10 @@ export async function getMapsTelemetry(config: MapsConfigType): Promise { const savedObjectsWithIndexPatternIds = savedObjects.map((savedObject) => { - return injectReferences(savedObject); + return { + ...savedObject, + ...injectReferences(savedObject), + }; }); return layerLists.push(...getLayerLists(savedObjectsWithIndexPatternIds)); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 139da00e48517..7698d11b10c22 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9512,7 +9512,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "検索可能スナップショット", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "検索可能なスナップショットを作成するには、エンタープライズライセンスが必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "エンタープライズライセンスが必要です", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotNoRolloverCalloutBody": "ロールオーバーがホットフェーズで無効な時には、検索可能なスナップショットを作成できません。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "スナップショットリポジトリ名が必要です。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel": "検索可能スナップショットを作成", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto": "リクエストを表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e747254e67ef4..208c0a2d4e7e5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9536,7 +9536,6 @@ "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotFieldTitle": "可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutBody": "要创建可搜索快照,需要企业许可证。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotLicenseCalloutTitle": "需要企业许可证", - "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotNoRolloverCalloutBody": "在热阶段禁用滚动更新后,无法创建可搜索快照。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotRepoRequiredError": "快照存储库名称必填。", "xpack.indexLifecycleMgmt.editPolicy.searchableSnapshotsToggleLabel": "创建可搜索快照", "xpack.indexLifecycleMgmt.editPolicy.showPolicyJsonButto": "显示请求", diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f03756a2885bb..41e94d69d2e9b 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -70,5 +70,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./calendars')); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./saved_objects')); + loadTestFile(require.resolve('./system')); }); } diff --git a/x-pack/test/api_integration/apis/ml/system/capabilities.ts b/x-pack/test/api_integration/apis/ml/system/capabilities.ts new file mode 100644 index 0000000000000..d8ab2a30ef7fb --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/capabilities.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + async function runRequest(user: USER): Promise { + const { body } = await supertest + .get(`/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities', () => { + describe('get capabilities', function () { + it('should be enabled in space', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + + it('should have upgradeInProgress false', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER); + + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER); + + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/system/index.ts b/x-pack/test/api_integration/apis/ml/system/index.ts new file mode 100644 index 0000000000000..68ffd5fa267e9 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('system', function () { + loadTestFile(require.resolve('./capabilities')); + loadTestFile(require.resolve('./space_capabilities')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts new file mode 100644 index 0000000000000..cd922bf4bae92 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/system/space_capabilities.ts @@ -0,0 +1,222 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { MlCapabilitiesResponse } from '../../../../../plugins/ml/common/types/capabilities'; + +const idSpaceWithMl = 'space_with_ml'; +const idSpaceNoMl = 'space_no_ml'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertestWithoutAuth'); + const spacesService = getService('spaces'); + const ml = getService('ml'); + + async function runRequest(user: USER, space?: string): Promise { + const { body } = await supertest + .get(`${space ? `/s/${space}` : ''}/api/ml/ml_capabilities`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .expect(200); + + return body; + } + + describe('ml_capabilities in spaces', () => { + before(async () => { + await spacesService.create({ id: idSpaceWithMl, name: 'space_one', disabledFeatures: [] }); + await spacesService.create({ id: idSpaceNoMl, name: 'space_two', disabledFeatures: ['ml'] }); + }); + + after(async () => { + await spacesService.delete(idSpaceWithMl); + await spacesService.delete(idSpaceNoMl); + }); + + describe('get capabilities', function () { + it('should be enabled in space - space with ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(mlFeatureEnabledInSpace).to.eql(true); + }); + it('should not be enabled in space - space without ML', async () => { + const { mlFeatureEnabledInSpace } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(mlFeatureEnabledInSpace).to.eql(false); + }); + + it('should have upgradeInProgress false - space with ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(upgradeInProgress).to.eql(false); + }); + it('should have upgradeInProgress false - space without ML', async () => { + const { upgradeInProgress } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(upgradeInProgress).to.eql(false); + }); + + it('should have full license - space with ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + it('should have full license - space without ML', async () => { + const { isPlatinumOrTrialLicense } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(isPlatinumOrTrialLicense).to.eql(true); + }); + + it('should have the right number of capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + it('should have the right number of capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(Object.keys(capabilities).length).to.eql(29); + }); + + it('should get viewer capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get viewer capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_VIEWER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + + it('should get power user capabilities - space with ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceWithMl); + expect(capabilities).to.eql({ + canCreateJob: true, + canDeleteJob: true, + canOpenJob: true, + canCloseJob: true, + canUpdateJob: true, + canForecastJob: true, + canCreateDatafeed: true, + canDeleteDatafeed: true, + canStartStopDatafeed: true, + canUpdateDatafeed: true, + canPreviewDatafeed: true, + canGetFilters: true, + canCreateCalendar: true, + canDeleteCalendar: true, + canCreateFilter: true, + canDeleteFilter: true, + canCreateDataFrameAnalytics: true, + canDeleteDataFrameAnalytics: true, + canStartStopDataFrameAnalytics: true, + canCreateMlAlerts: true, + canAccessML: true, + canGetJobs: true, + canGetDatafeeds: true, + canGetCalendars: true, + canFindFileStructure: true, + canGetDataFrameAnalytics: true, + canGetAnnotations: true, + canCreateAnnotation: true, + canDeleteAnnotation: true, + }); + }); + + it('should get power user capabilities - space without ML', async () => { + const { capabilities } = await runRequest(USER.ML_POWERUSER, idSpaceNoMl); + expect(capabilities).to.eql({ + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canUpdateJob: false, + canForecastJob: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, + canStartStopDatafeed: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + canGetFilters: false, + canCreateCalendar: false, + canDeleteCalendar: false, + canCreateFilter: false, + canDeleteFilter: false, + canCreateDataFrameAnalytics: false, + canDeleteDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, + canCreateMlAlerts: false, + canAccessML: false, + canGetJobs: false, + canGetDatafeeds: false, + canGetCalendars: false, + canFindFileStructure: false, + canGetDataFrameAnalytics: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, + }); + }); + }); + }); +};